Repository: caelestia-dots/shell Branch: main Commit: b64421d614e9 Files: 367 Total size: 1.5 MB Directory structure: gitextract_jcsh79_s/ ├── .clang-format ├── .envrc ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── config.yml │ │ ├── feature.yml │ │ └── issue.yml │ └── workflows/ │ ├── check-format.yml │ ├── release.yml │ ├── update-flake-inputs.yml │ └── update-image.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets/ │ ├── pam.d/ │ │ ├── fprint │ │ └── passwd │ ├── shaders/ │ │ ├── fade.frag │ │ ├── fade.frag.qsb │ │ ├── opacitymask.frag │ │ └── opacitymask.frag.qsb │ └── wrap_term_launch.sh ├── components/ │ ├── Anim.qml │ ├── CAnim.qml │ ├── ConnectionHeader.qml │ ├── ConnectionInfoSection.qml │ ├── DashboardState.qml │ ├── DrawerVisibilities.qml │ ├── Logo.qml │ ├── MaterialIcon.qml │ ├── PropertyRow.qml │ ├── SectionContainer.qml │ ├── SectionHeader.qml │ ├── StateLayer.qml │ ├── StyledClippingRect.qml │ ├── StyledRect.qml │ ├── StyledText.qml │ ├── containers/ │ │ ├── StyledFlickable.qml │ │ ├── StyledListView.qml │ │ └── StyledWindow.qml │ ├── controls/ │ │ ├── CircularIndicator.qml │ │ ├── CircularProgress.qml │ │ ├── CollapsibleSection.qml │ │ ├── CustomMouseArea.qml │ │ ├── CustomSpinBox.qml │ │ ├── FilledSlider.qml │ │ ├── IconButton.qml │ │ ├── IconTextButton.qml │ │ ├── Menu.qml │ │ ├── MenuItem.qml │ │ ├── SpinBoxRow.qml │ │ ├── SplitButton.qml │ │ ├── SplitButtonRow.qml │ │ ├── StyledInputField.qml │ │ ├── StyledRadioButton.qml │ │ ├── StyledScrollBar.qml │ │ ├── StyledSlider.qml │ │ ├── StyledSwitch.qml │ │ ├── StyledTextField.qml │ │ ├── SwitchRow.qml │ │ ├── TextButton.qml │ │ ├── ToggleButton.qml │ │ ├── ToggleRow.qml │ │ └── Tooltip.qml │ ├── effects/ │ │ ├── ColouredIcon.qml │ │ ├── Colouriser.qml │ │ ├── Elevation.qml │ │ ├── InnerBorder.qml │ │ └── OpacityMask.qml │ ├── filedialog/ │ │ ├── CurrentItem.qml │ │ ├── DialogButtons.qml │ │ ├── FileDialog.qml │ │ ├── FolderContents.qml │ │ ├── HeaderBar.qml │ │ ├── Sidebar.qml │ │ └── Sizes.qml │ ├── images/ │ │ ├── CachingIconImage.qml │ │ └── CachingImage.qml │ ├── misc/ │ │ ├── CustomShortcut.qml │ │ └── Ref.qml │ └── widgets/ │ └── ExtraIndicator.qml ├── config/ │ ├── Appearance.qml │ ├── AppearanceConfig.qml │ ├── BackgroundConfig.qml │ ├── BarConfig.qml │ ├── BorderConfig.qml │ ├── Config.qml │ ├── ControlCenterConfig.qml │ ├── DashboardConfig.qml │ ├── GeneralConfig.qml │ ├── LauncherConfig.qml │ ├── LockConfig.qml │ ├── NotifsConfig.qml │ ├── OsdConfig.qml │ ├── ServiceConfig.qml │ ├── SessionConfig.qml │ ├── SidebarConfig.qml │ ├── UserPaths.qml │ ├── UtilitiesConfig.qml │ └── WInfoConfig.qml ├── extras/ │ ├── CMakeLists.txt │ └── version.cpp ├── flake.nix ├── modules/ │ ├── BatteryMonitor.qml │ ├── IdleMonitors.qml │ ├── Shortcuts.qml │ ├── areapicker/ │ │ ├── AreaPicker.qml │ │ └── Picker.qml │ ├── background/ │ │ ├── Background.qml │ │ ├── DesktopClock.qml │ │ ├── Visualiser.qml │ │ └── Wallpaper.qml │ ├── bar/ │ │ ├── Bar.qml │ │ ├── BarWrapper.qml │ │ ├── components/ │ │ │ ├── ActiveWindow.qml │ │ │ ├── Clock.qml │ │ │ ├── OsIcon.qml │ │ │ ├── Power.qml │ │ │ ├── Settings.qml │ │ │ ├── SettingsIcon.qml │ │ │ ├── StatusIcons.qml │ │ │ ├── Tray.qml │ │ │ ├── TrayItem.qml │ │ │ └── workspaces/ │ │ │ ├── ActiveIndicator.qml │ │ │ ├── OccupiedBg.qml │ │ │ ├── SpecialWorkspaces.qml │ │ │ ├── Workspace.qml │ │ │ └── Workspaces.qml │ │ └── popouts/ │ │ ├── ActiveWindow.qml │ │ ├── Audio.qml │ │ ├── Background.qml │ │ ├── Battery.qml │ │ ├── Bluetooth.qml │ │ ├── Content.qml │ │ ├── LockStatus.qml │ │ ├── Network.qml │ │ ├── PopoutState.qml │ │ ├── TrayMenu.qml │ │ ├── WirelessPassword.qml │ │ ├── Wrapper.qml │ │ └── kblayout/ │ │ ├── KbLayout.qml │ │ └── KbLayoutModel.qml │ ├── controlcenter/ │ │ ├── ControlCenter.qml │ │ ├── NavRail.qml │ │ ├── PaneRegistry.qml │ │ ├── Panes.qml │ │ ├── Session.qml │ │ ├── WindowFactory.qml │ │ ├── WindowTitle.qml │ │ ├── appearance/ │ │ │ ├── AppearancePane.qml │ │ │ └── sections/ │ │ │ ├── AnimationsSection.qml │ │ │ ├── BackgroundSection.qml │ │ │ ├── BorderSection.qml │ │ │ ├── ColorSchemeSection.qml │ │ │ ├── ColorVariantSection.qml │ │ │ ├── FontsSection.qml │ │ │ ├── ScalesSection.qml │ │ │ ├── ThemeModeSection.qml │ │ │ └── TransparencySection.qml │ │ ├── audio/ │ │ │ └── AudioPane.qml │ │ ├── bluetooth/ │ │ │ ├── BtPane.qml │ │ │ ├── Details.qml │ │ │ ├── DeviceList.qml │ │ │ └── Settings.qml │ │ ├── components/ │ │ │ ├── ConnectedButtonGroup.qml │ │ │ ├── DeviceDetails.qml │ │ │ ├── DeviceList.qml │ │ │ ├── PaneTransition.qml │ │ │ ├── ReadonlySlider.qml │ │ │ ├── SettingsHeader.qml │ │ │ ├── SliderInput.qml │ │ │ ├── SplitPaneLayout.qml │ │ │ ├── SplitPaneWithDetails.qml │ │ │ └── WallpaperGrid.qml │ │ ├── dashboard/ │ │ │ ├── DashboardPane.qml │ │ │ ├── GeneralSection.qml │ │ │ └── PerformanceSection.qml │ │ ├── launcher/ │ │ │ ├── LauncherPane.qml │ │ │ └── Settings.qml │ │ ├── network/ │ │ │ ├── EthernetDetails.qml │ │ │ ├── EthernetList.qml │ │ │ ├── EthernetPane.qml │ │ │ ├── EthernetSettings.qml │ │ │ ├── NetworkSettings.qml │ │ │ ├── NetworkingPane.qml │ │ │ ├── VpnDetails.qml │ │ │ ├── VpnList.qml │ │ │ ├── VpnSettings.qml │ │ │ ├── WirelessDetails.qml │ │ │ ├── WirelessList.qml │ │ │ ├── WirelessPane.qml │ │ │ ├── WirelessPasswordDialog.qml │ │ │ └── WirelessSettings.qml │ │ ├── state/ │ │ │ ├── BluetoothState.qml │ │ │ ├── EthernetState.qml │ │ │ ├── LauncherState.qml │ │ │ ├── NetworkState.qml │ │ │ └── VpnState.qml │ │ └── taskbar/ │ │ └── TaskbarPane.qml │ ├── dashboard/ │ │ ├── Background.qml │ │ ├── Content.qml │ │ ├── Dash.qml │ │ ├── LyricMenu.qml │ │ ├── LyricsView.qml │ │ ├── Media.qml │ │ ├── MediaWrapper.qml │ │ ├── Performance.qml │ │ ├── Tabs.qml │ │ ├── Weather.qml │ │ ├── Wrapper.qml │ │ └── dash/ │ │ ├── Calendar.qml │ │ ├── DateTime.qml │ │ ├── Media.qml │ │ ├── Resources.qml │ │ ├── User.qml │ │ └── Weather.qml │ ├── drawers/ │ │ ├── Backgrounds.qml │ │ ├── Border.qml │ │ ├── Drawers.qml │ │ ├── Exclusions.qml │ │ ├── Interactions.qml │ │ └── Panels.qml │ ├── launcher/ │ │ ├── AppList.qml │ │ ├── Background.qml │ │ ├── Content.qml │ │ ├── ContentList.qml │ │ ├── WallpaperList.qml │ │ ├── Wrapper.qml │ │ ├── items/ │ │ │ ├── ActionItem.qml │ │ │ ├── AppItem.qml │ │ │ ├── CalcItem.qml │ │ │ ├── SchemeItem.qml │ │ │ ├── VariantItem.qml │ │ │ └── WallpaperItem.qml │ │ └── services/ │ │ ├── Actions.qml │ │ ├── Apps.qml │ │ ├── M3Variants.qml │ │ └── Schemes.qml │ ├── lock/ │ │ ├── Center.qml │ │ ├── Content.qml │ │ ├── Fetch.qml │ │ ├── InputField.qml │ │ ├── Lock.qml │ │ ├── LockSurface.qml │ │ ├── Media.qml │ │ ├── NotifDock.qml │ │ ├── NotifGroup.qml │ │ ├── Pam.qml │ │ ├── Resources.qml │ │ └── WeatherInfo.qml │ ├── notifications/ │ │ ├── Background.qml │ │ ├── Content.qml │ │ ├── Notification.qml │ │ └── Wrapper.qml │ ├── osd/ │ │ ├── Background.qml │ │ ├── Content.qml │ │ └── Wrapper.qml │ ├── session/ │ │ ├── Background.qml │ │ ├── Content.qml │ │ └── Wrapper.qml │ ├── sidebar/ │ │ ├── Background.qml │ │ ├── Content.qml │ │ ├── Notif.qml │ │ ├── NotifActionList.qml │ │ ├── NotifDock.qml │ │ ├── NotifDockList.qml │ │ ├── NotifGroup.qml │ │ ├── NotifGroupList.qml │ │ ├── Props.qml │ │ └── Wrapper.qml │ ├── utilities/ │ │ ├── Background.qml │ │ ├── Content.qml │ │ ├── RecordingDeleteModal.qml │ │ ├── Wrapper.qml │ │ ├── cards/ │ │ │ ├── IdleInhibit.qml │ │ │ ├── Record.qml │ │ │ ├── RecordingList.qml │ │ │ └── Toggles.qml │ │ └── toasts/ │ │ ├── ToastItem.qml │ │ └── Toasts.qml │ └── windowinfo/ │ ├── Buttons.qml │ ├── Details.qml │ ├── Preview.qml │ └── WindowInfo.qml ├── nix/ │ ├── default.nix │ └── hm-module.nix ├── plugin/ │ ├── CMakeLists.txt │ └── src/ │ └── Caelestia/ │ ├── CMakeLists.txt │ ├── Internal/ │ │ ├── CMakeLists.txt │ │ ├── arcgauge.cpp │ │ ├── arcgauge.hpp │ │ ├── cachingimagemanager.cpp │ │ ├── cachingimagemanager.hpp │ │ ├── circularbuffer.cpp │ │ ├── circularbuffer.hpp │ │ ├── circularindicatormanager.cpp │ │ ├── circularindicatormanager.hpp │ │ ├── hyprdevices.cpp │ │ ├── hyprdevices.hpp │ │ ├── hyprextras.cpp │ │ ├── hyprextras.hpp │ │ ├── logindmanager.cpp │ │ ├── logindmanager.hpp │ │ ├── sparklineitem.cpp │ │ └── sparklineitem.hpp │ ├── Models/ │ │ ├── CMakeLists.txt │ │ ├── filesystemmodel.cpp │ │ └── filesystemmodel.hpp │ ├── Services/ │ │ ├── CMakeLists.txt │ │ ├── audiocollector.cpp │ │ ├── audiocollector.hpp │ │ ├── audioprovider.cpp │ │ ├── audioprovider.hpp │ │ ├── beattracker.cpp │ │ ├── beattracker.hpp │ │ ├── cavaprovider.cpp │ │ ├── cavaprovider.hpp │ │ ├── service.cpp │ │ ├── service.hpp │ │ ├── serviceref.cpp │ │ └── serviceref.hpp │ ├── appdb.cpp │ ├── appdb.hpp │ ├── cutils.cpp │ ├── cutils.hpp │ ├── imageanalyser.cpp │ ├── imageanalyser.hpp │ ├── qalculator.cpp │ ├── qalculator.hpp │ ├── requests.cpp │ ├── requests.hpp │ ├── toaster.cpp │ └── toaster.hpp ├── scripts/ │ └── qml-lint-conventions.py ├── services/ │ ├── Audio.qml │ ├── Brightness.qml │ ├── Colours.qml │ ├── GameMode.qml │ ├── Hypr.qml │ ├── IdleInhibitor.qml │ ├── LyricsService.qml │ ├── Network.qml │ ├── NetworkUsage.qml │ ├── Nmcli.qml │ ├── NotifData.qml │ ├── Notifs.qml │ ├── Players.qml │ ├── Recorder.qml │ ├── Screens.qml │ ├── SystemUsage.qml │ ├── Time.qml │ ├── VPN.qml │ ├── Visibilities.qml │ ├── Wallpapers.qml │ └── Weather.qml ├── shell.qml └── utils/ ├── Icons.qml ├── Images.qml ├── NetworkConnection.qml ├── Paths.qml ├── Searcher.qml ├── Strings.qml ├── SysInfo.qml └── scripts/ ├── fuzzysort.js ├── fzf.js └── lrcparser.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- BasedOnStyle: LLVM IndentWidth: 4 ColumnLimit: 120 --- Language: Cpp DerivePointerAlignment: false PointerAlignment: Left AccessModifierOffset: -4 AlignAfterOpenBracket: DontAlign AllowShortEnumsOnASingleLine: false AllowShortFunctionsOnASingleLine: Inline AllowShortLambdasOnASingleLine: None BinPackArguments: true BreakBeforeBraces: Attach BreakConstructorInitializers: BeforeComma Cpp11BracedListStyle: false EmptyLineAfterAccessModifier: Never EmptyLineBeforeAccessModifier: Always IndentAccessModifiers: false IndentCaseLabels: false InsertNewlineAtEOF: true SeparateDefinitionBlocks: Always WrapNamespaceBodyWithEmptyLines: Always ... ================================================ FILE: .envrc ================================================ if has nix; then use flake fi shopt -s globstar watch_file assets/cpp/**/*.cpp watch_file assets/cpp/**/*.hpp watch_file plugin/**/*.cpp watch_file plugin/**/*.hpp watch_file **/CMakeLists.txt cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv cmake --build build export CAELESTIA_LIB_DIR="$PWD/build/lib" export QML2_IMPORT_PATH="$PWD/build/qml:${QML2_IMPORT_PATH:-}" ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing There are only a few rules: - Follow the commit convention as follows: - The name of the commit should be `module: change` - Try to be consistent with the module names; you can look at existing commits for the module names I use - If there is more than one change, the change in the commit name should be the most impactful change - Put other changes in the description - Format your code - I use the vscode qml extension with default arguments to format the code, however you do not have to use it - Just try to follow the code style of the rest of the code and ensure that there is: - no trailing whitespace on any lines - a single space between operators - No AI slop allowed - AI readme/docs slop = instant block - PLEASE TEST YOUR PRS - I can't believe I have to put this here, but please test your PRs before submitting them - Your PR must not break anything currently existing, or specify in the description if it does - PR descriptions should be descriptive - Please explain what the PR does and how to use it in your PR description - Also include any breaking changes and/or side effects of the PR ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: soramanew patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: soramane tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: soramane thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: Feature request description: Suggest a new feature labels: ["enhancement"] type: "Feature" title: "[FEATURE] " body: - type: markdown attributes: value: "NOTE: Please write in **English**." - type: textarea attributes: label: "What would you like to be added?" description: "Can be a suggestion for an existing feature. You can suggest a widget, minor user interaction changes.. whatever." - type: textarea attributes: label: "How will it help?" description: "It's helpful to include examples (like in your use case)." - type: textarea attributes: label: "Extra info" description: "If you want a new widget, a pic of the inspiration (if available) would be awesome." ================================================ FILE: .github/ISSUE_TEMPLATE/issue.yml ================================================ name: Issue description: Report an issue with the dots labels: ["bug"] type: "Bug" title: "[BUG] " body: - type: markdown attributes: 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." - type: checkboxes attributes: label: "Step 1. Before you submit" description: "Hint: The 2nd and 3rd checkbox is **not** forcely required as you may have failed to do so." options: - label: I have read the above instructions and am sure that this is supposed to be posted here. required: true - label: I've successfully updated to the latest versions following the [updating guide](https://github.com/caelestia-dots/caelestia?tab=readme-ov-file#updating). required: false # Not required cuz user may have failed to do so - label: I've successfully updated the system packages to the latest. required: false # Not required cuz user may have failed to do so - label: I've ticked the checkboxes without reading their contents required: false # Obviously - type: textarea attributes: label: "Step 2. Version info" description: "Run `caelestia -v` and paste the result below." value: "
Version info\n\n```\n\n```\n\n
" validations: required: true - type: markdown attributes: value: | **Tips for the following Step 3** 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. 2. If it throws errors, **PLEASE**, attach logs and describe in detail if possible. - Something happened to the shell (bar, dashboard, etc)? Run `caelestia shell -l` WITHOUT exiting the shell for logs. - Installation failed? Run installation again for logs. - You may use more code blocks when needed. 3. In case you are confused, the `
`, ``, ``, `
` are HTML tags for folding the logs (typically very long) inside. Please do not touch them (unless you know what you are doing). 4. If the logs are suuuuuuper long, consider using an online pastebin service instead. - type: textarea attributes: label: "Step 3. Describe the issue" value: "\n\n\n
Logs\n\n```\n\n```\n\n
" validations: required: true - type: checkboxes attributes: label: Reminder options: - label: I agree that it's usually impossible for others to help me without my logs. required: true ================================================ FILE: .github/workflows/check-format.yml ================================================ name: Check formatting on: push: branches: - main pull_request: jobs: check-format: runs-on: ubuntu-latest container: image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest steps: - uses: actions/checkout@v6 - name: Check QML format shell: fish {0} run: | for file in (string match -v 'build/*' **.qml) /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 end python3 scripts/qml-lint-conventions.py - name: Check C++ format shell: fish {0} run: | for file in (string match -v 'build/*' **.cpp **.hpp) clang-format $file | diff -u $file - || exit 1 end ================================================ FILE: .github/workflows/release.yml ================================================ name: Create release on: push: tags: - "v*" jobs: build-and-release: runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v6 - name: Create packages run: | mkdir -p release rsync -av \ --exclude='release' \ --exclude='.*' \ --exclude='nix' \ --exclude='flake.lock' \ --exclude='flake.nix' \ . release tar -czf caelestia-shell-${{ github.ref_name }}.tar.gz release cp caelestia-shell-${{ github.ref_name }}.tar.gz caelestia-shell-latest.tar.gz - name: Create release uses: softprops/action-gh-release@v2 with: files: | caelestia-shell-${{ github.ref_name }}.tar.gz caelestia-shell-latest.tar.gz generate_release_notes: true ================================================ FILE: .github/workflows/update-flake-inputs.yml ================================================ name: Update flake inputs on: workflow_dispatch: schedule: - cron: "0 0 * * 0" jobs: update-flake: runs-on: ubuntu-latest steps: - name: Generate app token id: app-token uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} persist-credentials: false - name: Install Nix uses: nixbuild/nix-quick-install-action@v31 with: nix_conf: | keep-env-derivations = true keep-outputs = true - name: Restore and save Nix store uses: nix-community/cache-nix-action@v6 with: # restore and save a cache using this key primary-key: nix-${{ hashFiles('**/*.nix', '**/flake.lock') }} # if there's no cache hit, restore a cache by this prefix restore-prefixes-first-match: nix- # collect garbage until the Nix store size (in bytes) is at most this number # before trying to save a new cache # 1G = 1073741824 gc-max-store-size-linux: 1G # do purge caches purge: true # purge all versions of the cache purge-prefixes: nix- # created more than this number of seconds ago purge-created: 0 # or, last accessed more than this number of seconds ago # relative to the start of the `Post Restore and save Nix store` phase purge-last-accessed: 0 # except any version with the key that is the same as the `primary-key` purge-primary-key: never - name: Update flake inputs run: nix flake update - name: Attempt to build flake run: nix build - name: Test on Sway env: XDG_RUNTIME_DIR: /home/runner/runtime WLR_BACKENDS: headless WLR_LIBINPUT_NO_DEVICES: 1 WAYLAND_DISPLAY: wayland-1 run: | mkdir $XDG_RUNTIME_DIR chown $USER $XDG_RUNTIME_DIR chmod 0700 $XDG_RUNTIME_DIR nix profile install 'nixpkgs#sway' sway & sleep 3 # Give Sway some time to start result/bin/caelestia-shell -d sleep 3 # Give the shell some time to start (and die) pgrep .quickshell-wra # Fail job if shell died result/bin/caelestia-shell kill killall sway # Screw using IPC - name: Check for changes id: check run: echo modified=$(git diff --exit-code flake.lock &>/dev/null && echo 'false' || echo 'true') >> $GITHUB_OUTPUT - name: Commit and push changes if: steps.check.outputs.modified == 'true' uses: EndBug/add-and-commit@v9 with: github_token: ${{ steps.app-token.outputs.token }} add: flake.lock default_author: github_actions message: "[CI] chore: update flake" ================================================ FILE: .github/workflows/update-image.yml ================================================ name: Update Docker CI image on: workflow_dispatch: schedule: - cron: "0 0 * * 0" permissions: packages: write jobs: build-image: runs-on: ubuntu-latest steps: - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Write Dockerfile run: | cat > /tmp/Dockerfile <> /etc/sudoers && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ cd /home/builder/yay-bin && \ sudo -u builder makepkg -si --noconfirm && \ sudo -u builder yay -S --noconfirm quickshell-git && \ sudo -u builder yay -Yc --noconfirm && \ pacman -Rns --noconfirm yay-bin && \ sed -i '/builder ALL=(ALL) NOPASSWD:ALL/d' /etc/sudoers && \ userdel -r builder && \ pacman -Scc --noconfirm EOF - name: Build and push uses: docker/build-push-action@v7 with: context: . file: /tmp/Dockerfile push: true tags: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest ================================================ FILE: .gitignore ================================================ .direnv /result /.qmlls.ini build/ .cache/ logs ================================================ FILE: .vscode/settings.json ================================================ { "editor.defaultFormatter": "theqtcompany.qt-qml", "[cpp]": { "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" } } ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.19) if(NOT DEFINED VERSION) execute_process(COMMAND git describe --tags --abbrev=0 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ) if("${VERSION}" STREQUAL "") message(FATAL_ERROR "VERSION is not set and failed to get from git") endif() endif() if(NOT DEFINED GIT_REVISION) execute_process(COMMAND git rev-parse HEAD WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" OUTPUT_VARIABLE GIT_REVISION OUTPUT_STRIP_TRAILING_WHITESPACE ) if("${GIT_REVISION}" STREQUAL "") message(FATAL_ERROR "GIT_REVISION is not set and failed to get from git") endif() endif() string(REGEX REPLACE "^v" "" VERSION "${VERSION}") project(caelestia-shell VERSION ${VERSION} LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(DISTRIBUTOR "Unset" CACHE STRING "Distributor") set(ENABLE_MODULES "extras;plugin;shell" CACHE STRING "Modules to build/install") set(INSTALL_LIBDIR "usr/lib/caelestia" CACHE STRING "Library install dir") set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir") set(INSTALL_QSCONFDIR "etc/xdg/quickshell/caelestia" CACHE STRING "Quickshell config install dir") add_compile_options( -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wold-style-cast -Wnull-dereference -Wdouble-promotion -Wformat=2 -Wfloat-equal -Woverloaded-virtual -Wsign-conversion -Wredundant-decls -Wswitch -Wunreachable-code ) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wunused-lambda-capture) endif() if("extras" IN_LIST ENABLE_MODULES) add_subdirectory(extras) endif() if("plugin" IN_LIST ENABLE_MODULES) add_subdirectory(plugin) endif() if("shell" IN_LIST ENABLE_MODULES) foreach(dir assets components config modules services utils) install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") endforeach() install(FILES shell.qml LICENSE DESTINATION "${INSTALL_QSCONFDIR}") endif() ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================

caelestia-shell

![GitHub last commit](https://img.shields.io/github/last-commit/caelestia-dots/shell?style=for-the-badge&labelColor=101418&color=9ccbfb) ![GitHub Repo stars](https://img.shields.io/github/stars/caelestia-dots/shell?style=for-the-badge&labelColor=101418&color=b9c8da) ![GitHub repo size](https://img.shields.io/github/repo-size/caelestia-dots/shell?style=for-the-badge&labelColor=101418&color=d3bfe6) [![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) [![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)
https://github.com/user-attachments/assets/0840f496-575c-4ca6-83a8-87bb01a85c5f ## Components - Widgets: [`Quickshell`](https://quickshell.outfoxxed.me) - Window manager: [`Hyprland`](https://hyprland.org) - Dots: [`caelestia`](https://github.com/caelestia-dots) ## Installation > [!NOTE] > This repo is for the desktop shell of the caelestia dots. If you want installation instructions > for the entire dots, head to [the main repo](https://github.com/caelestia-dots/caelestia) instead. ### Arch linux > [!NOTE] > If you want to make your own changes/tweaks to the shell do NOT edit the files installed by the AUR > package. Instead, follow the instructions in the [manual installation section](#manual-installation). The shell is available from the AUR as `caelestia-shell`. You can install it with an AUR helper like [`yay`](https://github.com/Jguer/yay) or manually downloading the PKGBUILD and running `makepkg -si`. A package following the latest commit also exists as `caelestia-shell-git`. This is bleeding edge and likely to be unstable/have bugs. Regular users are recommended to use the stable package (`caelestia-shell`). ### Nix You can run the shell directly via `nix run`: ```sh nix run github:caelestia-dots/shell ``` Or add it to your system configuration: ```nix { inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; caelestia-shell = { url = "github:caelestia-dots/shell"; inputs.nixpkgs.follows = "nixpkgs"; }; }; } ``` The package is available as `caelestia-shell.packages..default`, which can be added to your `environment.systemPackages`, `users.users..packages`, `home.packages` if using home-manager, or a devshell. The shell can then be run via `caelestia-shell`. > [!TIP] > The default package does not have the CLI enabled by default, which is required for full funcionality. > To enable the CLI, use the `with-cli` package. For 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. ### Manual installation Dependencies: - [`caelestia-cli`](https://github.com/caelestia-dots/cli) - [`quickshell-git`](https://quickshell.outfoxxed.me) - this has to be the git version, not the latest tagged version - [`ddcutil`](https://github.com/rockowitz/ddcutil) - [`brightnessctl`](https://github.com/Hummer12007/brightnessctl) - [`app2unit`](https://github.com/Vladimir-csp/app2unit) - [`libcava`](https://github.com/LukashonakV/cava) - [`networkmanager`](https://networkmanager.dev) - [`lm-sensors`](https://github.com/lm-sensors/lm-sensors) - [`fish`](https://github.com/fish-shell/fish-shell) - [`aubio`](https://github.com/aubio/aubio) - [`libpipewire`](https://pipewire.org) - `glibc` - `qt6-declarative` - `gcc-libs` - [`material-symbols`](https://fonts.google.com/icons) - [`caskaydia-cove-nerd`](https://www.nerdfonts.com/font-downloads) - [`swappy`](https://github.com/jtheoof/swappy) - [`libqalculate`](https://github.com/Qalculate/libqalculate) - [`bash`](https://www.gnu.org/software/bash) - `qt6-base` - `qt6-declarative` Build dependencies: - [`cmake`](https://cmake.org) - [`ninja`](https://github.com/ninja-build/ninja) To install the shell manually, install all dependencies and clone this repo to `$XDG_CONFIG_HOME/quickshell/caelestia`. Then simply build and install using `cmake`. ```sh cd $XDG_CONFIG_HOME/quickshell git clone https://github.com/caelestia-dots/shell.git caelestia cd caelestia cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ cmake --build build sudo cmake --install build ``` > [!TIP] > You can customise the installation location via the `cmake` flags `INSTALL_LIBDIR`, `INSTALL_QMLDIR` and > `INSTALL_QSCONFDIR` for the libraries (the beat detector), QML plugin and Quickshell config directories > respectively. If changing the library directory, remember to set the `CAELESTIA_LIB_DIR` environment > variable to the custom directory when launching the shell. > > e.g. installing to `~/.config/quickshell/caelestia` for easy local changes: > > ```sh > mkdir -p ~/.config/quickshell/caelestia > cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ -DINSTALL_QSCONFDIR=~/.config/quickshell/caelestia > cmake --build build > sudo cmake --install build > sudo chown -R $USER ~/.config/quickshell/caelestia > ``` ## Usage The shell can be started via the `caelestia shell -d` command or `qs -c caelestia`. If the entire caelestia dots are installed, the shell will be autostarted on login via an `exec-once` in the hyprland config. ### Shortcuts/IPC All keybinds are accessible via Hyprland [global shortcuts](https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts). If using the entire caelestia dots, the keybinds are already configured for you. Otherwise, [this file](https://github.com/caelestia-dots/caelestia/blob/main/hypr/hyprland/keybinds.conf#L1-L39) contains an example on how to use global shortcuts. All IPC commands can be accessed via `caelestia shell ...`. For example ```sh caelestia shell mpris getActive trackTitle ``` The list of IPC commands can be shown via `caelestia shell -s`: ``` $ caelestia shell -s target drawers function toggle(drawer: string): void function list(): string target notifs function clear(): void target lock function lock(): void function unlock(): void function isLocked(): bool target mpris function playPause(): void function getActive(prop: string): string function next(): void function stop(): void function play(): void function list(): string function pause(): void function previous(): void target picker function openFreeze(): void function open(): void target wallpaper function set(path: string): void function get(): string function list(): string ``` ### PFP/Wallpapers The profile picture for the dashboard is read from the file `~/.face`, so to set it you can copy your image to there or set it via the dashboard. The wallpapers for the wallpaper switcher are read from `~/Pictures/Wallpapers` by default. To change it, change the wallpapers path in `~/.config/caelestia/shell.json`. To set the wallpaper, you can use the command `caelestia wallpaper`. Use `caelestia wallpaper -h` for more info about the command. ## Updating If installed via the AUR package, simply update your system (e.g. using `yay`). If installed manually, you can update by running `git pull` in `$XDG_CONFIG_HOME/quickshell/caelestia`. ```sh cd $XDG_CONFIG_HOME/quickshell/caelestia git pull ``` ## Configuring All configuration options should be put in `~/.config/caelestia/shell.json`. This file is _not_ created by default, you must create it manually. ### Example configuration > [!NOTE] > The example configuration only includes recommended configuration options. For more advanced customisation > such as modifying the size of individual items or changing constants in the code, there are some other > options which can be found in the source files in the `config` directory.
Example ```json { "appearance": { "mediaGifSpeedAdjustment": 300, "sessionGifSpeed": 0.7, "anim": { "durations": { "scale": 1 } }, "font": { "family": { "clock": "Rubik", "material": "Material Symbols Rounded", "mono": "CaskaydiaCove NF", "sans": "Rubik" }, "size": { "scale": 1 } }, "padding": { "scale": 1 }, "rounding": { "scale": 1 }, "spacing": { "scale": 1 }, "transparency": { "enabled": false, "base": 0.85, "layers": 0.4 } }, "general": { "logo": "caelestia", "apps": { "terminal": ["foot"], "audio": ["pavucontrol"], "playback": ["mpv"], "explorer": ["thunar"] }, "battery": { "warnLevels": [ { "level": 20, "title": "Low battery", "message": "You might want to plug in a charger", "icon": "battery_android_frame_2" }, { "level": 10, "title": "Did you see the previous message?", "message": "You should probably plug in a charger now", "icon": "battery_android_frame_1" }, { "level": 5, "title": "Critical battery level", "message": "PLUG THE CHARGER RIGHT NOW!!", "icon": "battery_android_alert", "critical": true } ], "criticalLevel": 3 }, "idle": { "lockBeforeSleep": true, "inhibitWhenAudio": true, "timeouts": [ { "timeout": 180, "idleAction": "lock" }, { "timeout": 300, "idleAction": "dpms off", "returnAction": "dpms on" }, { "timeout": 600, "idleAction": ["systemctl", "suspend-then-hibernate"] } ] } }, "background": { "desktopClock": { "enabled": false, "scale": 1.0, "position": "bottom-right", "shadow": { "enabled": true, "opacity": 0.7, "blur": 0.4 }, "background": { "enabled": false, "opacity": 0.7, "blur": true }, "invertColors": false }, "enabled": true, "visualiser": { "blur": false, "enabled": false, "autoHide": true, "rounding": 1, "spacing": 1 } }, "bar": { "activeWindow": { "compact": false, "inverted": false, "showOnHover": true }, "clock": { "background": false, "showDate": false, "showIcon": true }, "dragThreshold": 20, "entries": [ { "id": "logo", "enabled": true }, { "id": "workspaces", "enabled": true }, { "id": "spacer", "enabled": true }, { "id": "activeWindow", "enabled": true }, { "id": "spacer", "enabled": true }, { "id": "tray", "enabled": true }, { "id": "clock", "enabled": true }, { "id": "statusIcons", "enabled": true }, { "id": "power", "enabled": true } ], "persistent": true, "popouts": { "activeWindow": true, "statusIcons": true, "tray": true }, "scrollActions": { "brightness": true, "workspaces": true, "volume": true }, "showOnHover": true, "status": { "showAudio": false, "showBattery": true, "showBluetooth": true, "showKbLayout": false, "showMicrophone": false, "showNetwork": true, "showWifi": true, "showLockStatus": true }, "tray": { "background": false, "compact": false, "iconSubs": [], "recolour": false }, "workspaces": { "activeIndicator": true, "activeLabel": "󰮯", "activeTrail": false, "label": " ", "occupiedBg": false, "occupiedLabel": "󰮯", "perMonitorWorkspaces": true, "showWindows": true, "shown": 5, "specialWorkspaceIcons": [ { "name": "steam", "icon": "sports_esports" } ], "windowIcons": [ { "regex": "steam(_app_(default|[0-9]+))?", "icon": "sports_esports" } ] }, "excludedScreens": [""], "activeWindow": { "inverted": false } }, "border": { "rounding": 25, "thickness": 10 }, "dashboard": { "enabled": true, "dragThreshold": 50, "mediaUpdateInterval": 500, "showOnHover": true }, "launcher": { "actionPrefix": ">", "actions": [ { "name": "Calculator", "icon": "calculate", "description": "Do simple math equations (powered by Qalc)", "command": ["autocomplete", "calc"], "enabled": true, "dangerous": false }, { "name": "Scheme", "icon": "palette", "description": "Change the current colour scheme", "command": ["autocomplete", "scheme"], "enabled": true, "dangerous": false }, { "name": "Wallpaper", "icon": "image", "description": "Change the current wallpaper", "command": ["autocomplete", "wallpaper"], "enabled": true, "dangerous": false }, { "name": "Variant", "icon": "colors", "description": "Change the current scheme variant", "command": ["autocomplete", "variant"], "enabled": true, "dangerous": false }, { "name": "Transparency", "icon": "opacity", "description": "Change shell transparency", "command": ["autocomplete", "transparency"], "enabled": false, "dangerous": false }, { "name": "Random", "icon": "casino", "description": "Switch to a random wallpaper", "command": ["caelestia", "wallpaper", "-r"], "enabled": true, "dangerous": false }, { "name": "Light", "icon": "light_mode", "description": "Change the scheme to light mode", "command": ["setMode", "light"], "enabled": true, "dangerous": false }, { "name": "Dark", "icon": "dark_mode", "description": "Change the scheme to dark mode", "command": ["setMode", "dark"], "enabled": true, "dangerous": false }, { "name": "Shutdown", "icon": "power_settings_new", "description": "Shutdown the system", "command": ["systemctl", "poweroff"], "enabled": true, "dangerous": true }, { "name": "Reboot", "icon": "cached", "description": "Reboot the system", "command": ["systemctl", "reboot"], "enabled": true, "dangerous": true }, { "name": "Logout", "icon": "exit_to_app", "description": "Log out of the current session", "command": ["loginctl", "terminate-user", ""], "enabled": true, "dangerous": true }, { "name": "Lock", "icon": "lock", "description": "Lock the current session", "command": ["loginctl", "lock-session"], "enabled": true, "dangerous": false }, { "name": "Sleep", "icon": "bedtime", "description": "Suspend then hibernate", "command": ["systemctl", "suspend-then-hibernate"], "enabled": true, "dangerous": false }, { "name": "Settings", "icon": "settings", "description": "Configure the shell", "command": ["caelestia", "shell", "controlCenter", "open"], "enabled": true, "dangerous": false } ], "dragThreshold": 50, "vimKeybinds": false, "enableDangerousActions": false, "maxShown": 7, "maxWallpapers": 9, "specialPrefix": "@", "useFuzzy": { "apps": false, "actions": false, "schemes": false, "variants": false, "wallpapers": false }, "showOnHover": false, "favouriteApps": [], "hiddenApps": [] }, "lock": { "recolourLogo": false, "hideNotifs": false }, "notifs": { "actionOnClick": false, "clearThreshold": 0.3, "defaultExpireTimeout": 5000, "expandThreshold": 20, "openExpanded": false, "expire": false }, "osd": { "enabled": true, "enableBrightness": true, "enableMicrophone": false, "hideDelay": 2000 }, "paths": { "mediaGif": "root:/assets/bongocat.gif", "sessionGif": "root:/assets/kurukuru.gif", "wallpaperDir": "~/Pictures/Wallpapers", "lyricsDir": "~/Music/lyrics" }, "services": { "audioIncrement": 0.1, "brightnessIncrement": 0.1, "maxVolume": 1.0, "defaultPlayer": "Spotify", "gpuType": "", "playerAliases": [{ "from": "com.github.th_ch.youtube_music", "to": "YT Music" }], "weatherLocation": "", "useFahrenheit": false, "useFahrenheitPerformance": false, "useTwelveHourClock": false, "smartScheme": true, "visualiserBars": 45 }, "session": { "dragThreshold": 30, "enabled": true, "vimKeybinds": false, "icons": { "logout": "logout", "shutdown": "power_settings_new", "hibernate": "downloading", "reboot": "cached" }, "commands": { "logout": ["loginctl", "terminate-user", ""], "shutdown": ["systemctl", "poweroff"], "hibernate": ["systemctl", "hibernate"], "reboot": ["systemctl", "reboot"] } }, "sidebar": { "dragThreshold": 80, "enabled": true }, "utilities": { "enabled": true, "maxToasts": 4, "toasts": { "audioInputChanged": true, "audioOutputChanged": true, "capsLockChanged": true, "chargingChanged": true, "configLoaded": true, "dndChanged": true, "gameModeChanged": true, "kbLayoutChanged": true, "kbLimit": true, "numLockChanged": true, "vpnChanged": true, "nowPlaying": false }, "vpn": { "enabled": true, "provider": [ { "name": "wireguard", "interface": "your-connection-name", "displayName": "Wireguard (Your VPN)", "enabled": false } ] }, "quickToggles": [ { "id": "wifi", "enabled": true }, { "id": "bluetooth", "enabled": true }, { "id": "mic", "enabled": true }, { "enabled": true, "id": "settings" }, { "id": "gameMode", "enabled": true }, { "id": "dnd", "enabled": true }, { "id": "vpn", "enabled": true } ] } } ```
### Home Manager Module For NixOS users, a home manager module is also available.
home.nix ```nix programs.caelestia = { enable = true; systemd = { enable = false; # if you prefer starting from your compositor target = "graphical-session.target"; environment = []; }; settings = { bar.status = { showBattery = false; }; paths.wallpaperDir = "~/Images"; }; cli = { enable = true; # Also add caelestia-cli to path settings = { theme.enableGtk = false; }; }; }; ``` The 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.
## FAQ ### Need help or support? You can join the community Discord server for assistance and discussion: https://discord.gg/BGDCFCmMBk ### My screen is flickering, help pls! Try disabling VRR in the hyprland config. You can do this by adding the following to `~/.config/caelestia/hypr-user.conf`: ```conf misc { vrr = 0 } ``` ### I want to make my own changes to the hyprland config! You can add your custom hyprland configs to `~/.config/caelestia/hypr-user.conf`. ### I want to make my own changes to other stuff! See the [manual installation](https://github.com/caelestia-dots/shell?tab=readme-ov-file#manual-installation) section for the corresponding repo. ### I want to disable XXX feature! Please read the [configuring](https://github.com/caelestia-dots/shell?tab=readme-ov-file#configuring) section in the readme. If there is no corresponding option, make feature request. ### How do I make my colour scheme change with my wallpaper? Set a wallpaper via the launcher or `caelestia wallpaper` and set the scheme to the dynamic scheme via the launcher or `caelestia scheme set`. e.g. ```sh caelestia wallpaper -f caelestia scheme set -n dynamic ``` ### My wallpapers aren't showing up in the launcher! The launcher pulls wallpapers from `~/Pictures/Wallpapers` by default. You can change this in the config. Additionally, the launcher only shows an odd number of wallpapers at one time. If you only have 2 wallpapers, consider getting more (or just putting one). ## Credits Thanks to the Hyprland discord community (especially the homies in #rice-discussion) for all the help and suggestions for improving these dots! A special thanks to [@outfoxxed](https://github.com/outfoxxed) for making Quickshell and the effort put into fixing issues and implementing various feature requests. Another special thanks to [@end_4](https://github.com/end-4) for his [config](https://github.com/end-4/dots-hyprland) which helped me a lot with learning how to use Quickshell. Finally another thank you to all the configs I took inspiration from (only one for now): - [Axenide/Ax-Shell](https://github.com/Axenide/Ax-Shell) ## Stonks 📈 Star History Chart ================================================ FILE: assets/pam.d/fprint ================================================ #%PAM-1.0 auth required pam_fprintd.so max-tries=1 ================================================ FILE: assets/pam.d/passwd ================================================ #%PAM-1.0 auth required pam_faillock.so preauth auth [success=1 default=bad] pam_unix.so nullok auth [default=die] pam_faillock.so authfail auth required pam_faillock.so authsucc ================================================ FILE: assets/shaders/fade.frag ================================================ #version 440 layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) out vec4 fragColor; layout(std140, binding = 0) uniform buf { mat4 qt_Matrix; float qt_Opacity; float fadeMargin; }; layout(binding = 1) uniform sampler2D source; void main() { vec4 tex = texture(source, qt_TexCoord0); float factor = 1.0; float margin = 0.1; if (qt_TexCoord0.y < margin) { factor = qt_TexCoord0.y / margin; } else if (qt_TexCoord0.y > (1.0 - margin)) { factor = (1.0 - qt_TexCoord0.y) / margin; } fragColor = tex * factor * qt_Opacity; } ================================================ FILE: assets/shaders/opacitymask.frag ================================================ #version 440 layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) out vec4 fragColor; layout(std140, binding = 0) uniform buf { // qt_Matrix and qt_Opacity must always be both present // if the built-in vertex shader is used. mat4 qt_Matrix; float qt_Opacity; }; layout(binding = 1) uniform sampler2D source; layout(binding = 2) uniform sampler2D maskSource; void main() { fragColor = texture(source, qt_TexCoord0.st) * (texture(maskSource, qt_TexCoord0.st).a) * qt_Opacity; } ================================================ FILE: assets/wrap_term_launch.sh ================================================ #!/usr/bin/env sh cat ~/.local/state/caelestia/sequences.txt 2>/dev/null exec "$@" ================================================ FILE: components/Anim.qml ================================================ import qs.config import QtQuick NumberAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } ================================================ FILE: components/CAnim.qml ================================================ import qs.config import QtQuick ColorAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } ================================================ FILE: components/ConnectionHeader.qml ================================================ import qs.components import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property string icon required property string title spacing: Appearance.spacing.normal Layout.alignment: Qt.AlignHCenter MaterialIcon { Layout.alignment: Qt.AlignHCenter animate: true text: root.icon font.pointSize: Appearance.font.size.extraLarge * 3 font.bold: true } StyledText { Layout.alignment: Qt.AlignHCenter animate: true text: root.title font.pointSize: Appearance.font.size.large font.bold: true } } ================================================ FILE: components/ConnectionInfoSection.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property var deviceDetails spacing: Appearance.spacing.small / 2 StyledText { text: qsTr("IP Address") } StyledText { text: root.deviceDetails?.ipAddress || qsTr("Not available") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Subnet Mask") } StyledText { text: root.deviceDetails?.subnet || qsTr("Not available") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Gateway") } StyledText { text: root.deviceDetails?.gateway || qsTr("Not available") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("DNS Servers") } StyledText { text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small wrapMode: Text.Wrap Layout.maximumWidth: parent.width } } ================================================ FILE: components/DashboardState.qml ================================================ import Quickshell PersistentProperties { property int currentTab property date currentDate: new Date() } ================================================ FILE: components/DrawerVisibilities.qml ================================================ import Quickshell PersistentProperties { property bool bar property bool osd property bool session property bool launcher property bool dashboard property bool utilities property bool sidebar } ================================================ FILE: components/Logo.qml ================================================ import QtQuick import QtQuick.Shapes import qs.services Item { id: root readonly property real designWidth: 128 readonly property real designHeight: 90.38 property color topColour: Colours.palette.m3primary property color bottomColour: Colours.palette.m3onSurface implicitWidth: designWidth implicitHeight: designHeight Shape { anchors.centerIn: parent width: root.designWidth height: root.designHeight scale: Math.min(root.width / width, root.height / height) transformOrigin: Item.Center preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: root.topColour strokeColor: "transparent" PathSvg { 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" } } ShapePath { fillColor: root.bottomColour strokeColor: "transparent" PathSvg { 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" } } ShapePath { fillColor: root.topColour strokeColor: "transparent" PathSvg { 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" } } ShapePath { fillColor: root.topColour strokeColor: "transparent" PathSvg { 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" } } ShapePath { fillColor: root.topColour strokeColor: "transparent" PathSvg { 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" } } } } ================================================ FILE: components/MaterialIcon.qml ================================================ import qs.services import qs.config StyledText { property real fill property int grade: Colours.light ? 0 : -25 font.family: Appearance.font.family.material font.pointSize: Appearance.font.size.larger font.variableAxes: ({ FILL: fill.toFixed(1), GRAD: grade, opsz: fontInfo.pixelSize, wght: fontInfo.weight }) } ================================================ FILE: components/PropertyRow.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property string label required property string value property bool showTopMargin: false spacing: Appearance.spacing.small / 2 StyledText { Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 text: root.label } StyledText { text: root.value color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } } ================================================ FILE: components/SectionContainer.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root default property alias content: contentColumn.data property real contentSpacing: Appearance.spacing.larger property bool alignTop: false Layout.fillWidth: true implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh ColumnLayout { id: contentColumn anchors.left: parent.left anchors.right: parent.right anchors.top: root.alignTop ? parent.top : undefined anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter anchors.margins: Appearance.padding.large spacing: root.contentSpacing } } ================================================ FILE: components/SectionHeader.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property string title property string description: "" spacing: 0 StyledText { Layout.topMargin: Appearance.spacing.large text: root.title font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { visible: root.description !== "" text: root.description color: Colours.palette.m3outline } } ================================================ FILE: components/StateLayer.qml ================================================ import qs.services import qs.config import QtQuick MouseArea { id: root property bool disabled property bool showHoverBackground: true property color color: Colours.palette.m3onSurface // Pick up radius from parent if it has one (parent can be anything with a radius property) property real radius: parent?.radius ?? 0 // qmllint disable missing-property property alias rect: hoverLayer function onClicked(): void { } anchors.fill: parent enabled: !disabled cursorShape: disabled ? undefined : Qt.PointingHandCursor hoverEnabled: true onPressed: event => { if (disabled) return; rippleAnim.x = event.x; rippleAnim.y = event.y; const dist = (ox, oy) => ox * ox + oy * oy; 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))); rippleAnim.restart(); } onClicked: event => !disabled && onClicked(event) SequentialAnimation { id: rippleAnim property real x property real y property real radius PropertyAction { target: ripple property: "x" value: rippleAnim.x } PropertyAction { target: ripple property: "y" value: rippleAnim.y } PropertyAction { target: ripple property: "opacity" value: 0.08 } Anim { target: ripple properties: "implicitWidth,implicitHeight" from: 0 to: rippleAnim.radius * 2 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { target: ripple property: "opacity" to: 0 } } StyledClippingRect { id: hoverLayer anchors.fill: parent color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) radius: root.radius StyledRect { id: ripple radius: Appearance.rounding.full color: root.color opacity: 0 transform: Translate { x: -ripple.width / 2 y: -ripple.height / 2 } } } } ================================================ FILE: components/StyledClippingRect.qml ================================================ import Quickshell.Widgets import QtQuick ClippingRectangle { id: root color: "transparent" Behavior on color { CAnim {} } } ================================================ FILE: components/StyledRect.qml ================================================ import QtQuick Rectangle { id: root color: "transparent" Behavior on color { CAnim {} } } ================================================ FILE: components/StyledText.qml ================================================ pragma ComponentBehavior: Bound import qs.services import qs.config import QtQuick Text { id: root property bool animate: false property string animateProp: "scale" property real animateFrom: 0 property real animateTo: 1 property int animateDuration: Appearance.anim.durations.normal renderType: Text.NativeRendering textFormat: Text.PlainText color: Colours.palette.m3onSurface font.family: Appearance.font.family.sans font.pointSize: Appearance.font.size.smaller Behavior on color { CAnim {} } Behavior on text { enabled: root.animate SequentialAnimation { Anim { to: root.animateFrom easing.bezierCurve: Appearance.anim.curves.standardAccel } PropertyAction {} Anim { to: root.animateTo easing.bezierCurve: Appearance.anim.curves.standardDecel } } } component Anim: NumberAnimation { target: root property: root.animateProp duration: root.animateDuration / 2 easing.type: Easing.BezierSpline } } ================================================ FILE: components/containers/StyledFlickable.qml ================================================ import ".." import QtQuick Flickable { id: root maximumFlickVelocity: 3000 rebound: Transition { Anim { properties: "x,y" } } } ================================================ FILE: components/containers/StyledListView.qml ================================================ import ".." import QtQuick ListView { id: root maximumFlickVelocity: 3000 rebound: Transition { Anim { properties: "x,y" } } } ================================================ FILE: components/containers/StyledWindow.qml ================================================ import Quickshell import Quickshell.Wayland PanelWindow { required property string name WlrLayershell.namespace: `caelestia-${name}` color: "transparent" } ================================================ FILE: components/controls/CircularIndicator.qml ================================================ import ".." import qs.services import qs.config import Caelestia.Internal import QtQuick import QtQuick.Templates BusyIndicator { id: root enum AnimType { Advance = 0, Retreat } enum AnimState { Stopped, Running, Completing } property real implicitSize: Appearance.font.size.normal * 3 property real strokeWidth: Appearance.padding.small * 0.8 property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer property alias type: manager.indeterminateAnimationType readonly property alias progress: manager.progress property real internalStrokeWidth: strokeWidth property int animState padding: 0 implicitWidth: implicitSize implicitHeight: implicitSize Component.onCompleted: { if (running) { running = false; running = true; } } onRunningChanged: { if (running) { manager.completeEndProgress = 0; animState = CircularIndicator.Running; } else { if (animState == CircularIndicator.Running) animState = CircularIndicator.Completing; } } states: State { name: "stopped" when: !root.running PropertyChanges { root.opacity: 0 root.internalStrokeWidth: root.strokeWidth / 3 } } transitions: Transition { Anim { properties: "opacity,internalStrokeWidth" duration: manager.completeEndDuration * Appearance.anim.durations.scale } } contentItem: CircularProgress { anchors.fill: parent strokeWidth: root.internalStrokeWidth fgColour: root.fgColour bgColour: root.bgColour padding: root.padding rotation: manager.rotation startAngle: manager.startFraction * 360 value: manager.endFraction - manager.startFraction } CircularIndicatorManager { id: manager } NumberAnimation { running: root.animState !== CircularIndicator.Stopped loops: Animation.Infinite target: manager property: "progress" from: 0 to: 1 duration: manager.duration * Appearance.anim.durations.scale } NumberAnimation { running: root.animState === CircularIndicator.Completing target: manager property: "completeEndProgress" from: 0 to: 1 duration: manager.completeEndDuration * Appearance.anim.durations.scale onFinished: { if (root.animState === CircularIndicator.Completing) root.animState = CircularIndicator.Stopped; } } } ================================================ FILE: components/controls/CircularProgress.qml ================================================ import ".." import qs.services import qs.config import QtQuick import QtQuick.Shapes Shape { id: root property real value property int startAngle: -90 property int strokeWidth: Appearance.padding.smaller property int padding: 0 property int spacing: Appearance.spacing.small property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer readonly property real size: Math.min(width, height) readonly property real arcRadius: (size - padding - strokeWidth) / 2 readonly property real vValue: value || 1 / 360 readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) preferredRendererType: Shape.CurveRenderer asynchronous: true ShapePath { fillColor: "transparent" strokeColor: root.bgColour strokeWidth: root.strokeWidth capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle + 360 * root.vValue + root.gapAngle sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) radiusX: root.arcRadius radiusY: root.arcRadius centerX: root.size / 2 centerY: root.size / 2 } Behavior on strokeColor { CAnim { duration: Appearance.anim.durations.large } } } ShapePath { fillColor: "transparent" strokeColor: root.fgColour strokeWidth: root.strokeWidth capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle sweepAngle: 360 * root.vValue radiusX: root.arcRadius radiusY: root.arcRadius centerX: root.size / 2 centerY: root.size / 2 } Behavior on strokeColor { CAnim { duration: Appearance.anim.durations.large } } } } ================================================ FILE: components/controls/CollapsibleSection.qml ================================================ import ".." import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property string title property string description: "" property bool expanded: false property bool showBackground: false property bool nested: false default property alias content: contentColumn.data signal toggleRequested spacing: Appearance.spacing.small Layout.fillWidth: true Item { id: sectionHeaderItem Layout.fillWidth: true Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) RowLayout { id: titleRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.padding.normal anchors.rightMargin: Appearance.padding.normal spacing: Appearance.spacing.normal StyledText { text: root.title font.pointSize: Appearance.font.size.larger font.weight: 500 } Item { Layout.fillWidth: true } MaterialIcon { text: "expand_more" rotation: root.expanded ? 180 : 0 color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.normal Behavior on rotation { Anim { duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standard } } } } StateLayer { function onClicked(): void { root.toggleRequested(); root.expanded = !root.expanded; } anchors.fill: parent color: Colours.palette.m3onSurface radius: Appearance.rounding.normal showHoverBackground: false } } Item { id: contentWrapper Layout.fillWidth: true Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 clip: true Behavior on Layout.preferredHeight { Anim { easing.bezierCurve: Appearance.anim.curves.standard } } StyledRect { id: backgroundRect anchors.fill: parent radius: Appearance.rounding.normal color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) opacity: root.showBackground && root.expanded ? 1.0 : 0.0 visible: root.showBackground Behavior on opacity { Anim { easing.bezierCurve: Appearance.anim.curves.standard } } } ColumnLayout { id: contentColumn anchors.left: parent.left anchors.right: parent.right y: Appearance.spacing.small anchors.leftMargin: Appearance.padding.normal anchors.rightMargin: Appearance.padding.normal anchors.bottomMargin: Appearance.spacing.small spacing: Appearance.spacing.small opacity: root.expanded ? 1.0 : 0.0 Behavior on opacity { Anim { easing.bezierCurve: Appearance.anim.curves.standard } } StyledText { id: descriptionText Layout.fillWidth: true Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 visible: root.description !== "" text: root.description color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small wrapMode: Text.Wrap } } } } ================================================ FILE: components/controls/CustomMouseArea.qml ================================================ import QtQuick MouseArea { property int scrollAccumulatedY: 0 function onWheel(event: WheelEvent): void { } onWheel: event => { // Update accumulated scroll if (Math.sign(event.angleDelta.y) !== Math.sign(scrollAccumulatedY)) scrollAccumulatedY = 0; scrollAccumulatedY += event.angleDelta.y; // Trigger handler and reset if above threshold if (Math.abs(scrollAccumulatedY) >= 120) { onWheel(event); scrollAccumulatedY = 0; } } } ================================================ FILE: components/controls/CustomSpinBox.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.services import qs.config import QtQuick import QtQuick.Layouts RowLayout { id: root property real value property real max: Infinity property real min: -Infinity property real step: 1 property alias repeatRate: timer.interval property bool isEditing: false property string displayText: root.value.toString() signal valueModified(value: real) spacing: Appearance.spacing.small onValueChanged: { if (!root.isEditing) { root.displayText = root.value.toString(); } } StyledTextField { id: textField inputMethodHints: Qt.ImhFormattedNumbersOnly text: root.isEditing ? text : root.displayText validator: DoubleValidator { bottom: root.min top: root.max decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0 } onActiveFocusChanged: { if (activeFocus) { root.isEditing = true; } else { root.isEditing = false; root.displayText = root.value.toString(); } } onAccepted: { const numValue = parseFloat(text); if (!isNaN(numValue)) { const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); root.value = clampedValue; root.displayText = clampedValue.toString(); root.valueModified(clampedValue); } else { text = root.displayText; } root.isEditing = false; } onEditingFinished: { if (text !== root.displayText) { const numValue = parseFloat(text); if (!isNaN(numValue)) { const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); root.value = clampedValue; root.displayText = clampedValue.toString(); root.valueModified(clampedValue); } else { text = root.displayText; } } root.isEditing = false; } padding: Appearance.padding.small leftPadding: Appearance.padding.normal rightPadding: Appearance.padding.normal background: StyledRect { implicitWidth: 100 radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainerHigh } } StyledRect { radius: Appearance.rounding.small color: Colours.palette.m3primary implicitWidth: implicitHeight implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 StateLayer { id: upState function onClicked(): void { let newValue = Math.min(root.max, root.value + root.step); // Round to avoid floating point precision errors const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); root.value = newValue; root.displayText = newValue.toString(); root.valueModified(newValue); } color: Colours.palette.m3onPrimary onPressAndHold: timer.start() onReleased: timer.stop() } MaterialIcon { id: upIcon anchors.centerIn: parent text: "keyboard_arrow_up" color: Colours.palette.m3onPrimary } } StyledRect { radius: Appearance.rounding.small color: Colours.palette.m3primary implicitWidth: implicitHeight implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 StateLayer { id: downState function onClicked(): void { let newValue = Math.max(root.min, root.value - root.step); // Round to avoid floating point precision errors const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); root.value = newValue; root.displayText = newValue.toString(); root.valueModified(newValue); } color: Colours.palette.m3onPrimary onPressAndHold: timer.start() onReleased: timer.stop() } MaterialIcon { id: downIcon anchors.centerIn: parent text: "keyboard_arrow_down" color: Colours.palette.m3onPrimary } } Timer { id: timer interval: 100 repeat: true triggeredOnStart: true onTriggered: { if (upState.pressed) upState.onClicked(); else if (downState.pressed) downState.onClicked(); } } } ================================================ FILE: components/controls/FilledSlider.qml ================================================ import ".." import "../effects" import qs.services import qs.config import QtQuick import QtQuick.Templates Slider { id: root required property string icon property real oldValue property bool initialized orientation: Qt.Vertical background: StyledRect { color: Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.full StyledRect { anchors.left: parent.left anchors.right: parent.right y: root.handle.y implicitHeight: parent.height - y color: Colours.palette.m3secondary radius: parent.radius } } handle: Item { id: handle property alias moving: icon.moving y: root.visualPosition * (root.availableHeight - height) implicitWidth: root.width implicitHeight: root.width Elevation { anchors.fill: parent radius: rect.radius level: handleInteraction.containsMouse ? 2 : 1 } StyledRect { id: rect anchors.fill: parent color: Colours.palette.m3inverseSurface radius: Appearance.rounding.full MouseArea { id: handleInteraction anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.NoButton } MaterialIcon { id: icon property bool moving function update(): void { animate = !moving; binding.when = moving; font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; } text: root.icon color: Colours.palette.m3inverseOnSurface anchors.centerIn: parent onMovingChanged: anim.restart() Binding { id: binding target: icon property: "text" value: Math.round(root.value * 100) when: false } SequentialAnimation { id: anim Anim { target: icon property: "scale" to: 0 duration: Appearance.anim.durations.normal / 2 easing.bezierCurve: Appearance.anim.curves.standardAccel } ScriptAction { script: icon.update() } Anim { target: icon property: "scale" to: 1 duration: Appearance.anim.durations.normal / 2 easing.bezierCurve: Appearance.anim.curves.standardDecel } } } } } onPressedChanged: handle.moving = pressed onValueChanged: { if (!initialized) { initialized = true; return; } if (Math.abs(value - oldValue) < 0.01) return; oldValue = value; handle.moving = true; stateChangeDelay.restart(); } Timer { id: stateChangeDelay interval: 500 onTriggered: { if (!root.pressed) handle.moving = false; } } Behavior on value { Anim { duration: Appearance.anim.durations.large } } } ================================================ FILE: components/controls/IconButton.qml ================================================ import ".." import qs.services import qs.config import QtQuick StyledRect { id: root enum Type { Filled, Tonal, Text } property alias icon: label.text property bool checked property bool toggle property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller property alias font: label.font property int type: IconButton.Filled property bool disabled property alias stateLayer: stateLayer property alias label: label property alias radiusAnim: radiusAnim property bool internalChecked property color activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary property color inactiveColour: { if (!toggle && type === IconButton.Filled) return Colours.palette.m3primary; return type === IconButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; } property color activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary property color inactiveOnColour: { if (!toggle && type === IconButton.Filled) return Colours.palette.m3onPrimary; return type === IconButton.Tonal ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant; } property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) signal clicked onCheckedChanged: internalChecked = checked radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour implicitWidth: implicitHeight implicitHeight: label.implicitHeight + padding * 2 StateLayer { id: stateLayer function onClicked(): void { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour disabled: root.disabled } MaterialIcon { id: label anchors.centerIn: parent color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour fill: !root.toggle || root.internalChecked ? 1 : 0 Behavior on fill { Anim {} } } Behavior on radius { Anim { id: radiusAnim } } } ================================================ FILE: components/controls/IconTextButton.qml ================================================ import ".." import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root enum Type { Filled, Tonal, Text } property alias icon: iconLabel.text property alias text: label.text property bool checked property bool toggle property real horizontalPadding: Appearance.padding.normal property real verticalPadding: Appearance.padding.smaller property alias font: label.font property int type: IconTextButton.Filled property alias stateLayer: stateLayer property alias iconLabel: iconLabel property alias label: label property bool internalChecked property color activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary property color inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer property color activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary property color inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer signal clicked onCheckedChanged: internalChecked = checked radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour implicitWidth: row.implicitWidth + horizontalPadding * 2 implicitHeight: row.implicitHeight + verticalPadding * 2 StateLayer { id: stateLayer function onClicked(): void { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } RowLayout { id: row anchors.centerIn: parent spacing: Appearance.spacing.small MaterialIcon { id: iconLabel Layout.alignment: Qt.AlignVCenter Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour fill: root.internalChecked ? 1 : 0 Behavior on fill { Anim {} } } StyledText { id: label Layout.alignment: Qt.AlignVCenter Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575) color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } } Behavior on radius { Anim {} } } ================================================ FILE: components/controls/Menu.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../effects" import qs.services import qs.config import QtQuick import QtQuick.Layouts Elevation { id: root property list items property MenuItem active: items[0] ?? null property bool expanded signal itemSelected(item: MenuItem) radius: Appearance.rounding.small / 2 level: 2 implicitWidth: Math.max(200, column.implicitWidth) implicitHeight: root.expanded ? column.implicitHeight : 0 opacity: root.expanded ? 1 : 0 StyledClippingRect { anchors.fill: parent radius: parent.radius color: Colours.palette.m3surfaceContainer ColumnLayout { id: column anchors.left: parent.left anchors.right: parent.right spacing: 0 Repeater { model: root.items StyledRect { id: item required property int index required property MenuItem modelData readonly property bool active: modelData === root.active Layout.fillWidth: true implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) StateLayer { function onClicked(): void { root.itemSelected(item.modelData); root.active = item.modelData; root.expanded = false; } color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface disabled: !root.expanded } RowLayout { id: menuOptionRow anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.small MaterialIcon { Layout.alignment: Qt.AlignVCenter text: item.modelData.icon color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant } StyledText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true text: item.modelData.text color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } Loader { asynchronous: true Layout.alignment: Qt.AlignVCenter active: item.modelData.trailingIcon.length > 0 visible: active sourceComponent: MaterialIcon { text: item.modelData.trailingIcon color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } } } } } } } Behavior on opacity { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } ================================================ FILE: components/controls/MenuItem.qml ================================================ import QtQuick QtObject { required property string text property string icon property string trailingIcon property string activeIcon: icon property string activeText: text signal clicked } ================================================ FILE: components/controls/SpinBoxRow.qml ================================================ import ".." import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property string label required property real value required property real min required property real max property real step: 1 property var onValueModified: function (value) {} Layout.fillWidth: true implicitHeight: row.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: row anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: root.label } CustomSpinBox { min: root.min max: root.max step: root.step value: root.value onValueModified: value => { root.onValueModified(value); } } } } ================================================ FILE: components/controls/SplitButton.qml ================================================ import ".." import qs.services import qs.config import QtQuick import QtQuick.Layouts Row { id: root enum Type { Filled, Tonal } property real horizontalPadding: Appearance.padding.normal property real verticalPadding: Appearance.padding.smaller property int type: SplitButton.Filled property bool disabled property bool menuOnTop property string fallbackIcon property string fallbackText property alias menuItems: menu.items property alias active: menu.active property alias expanded: menu.expanded property alias menu: menu property alias iconLabel: iconLabel property alias label: label property alias stateLayer: stateLayer property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) spacing: Math.floor(Appearance.spacing.small / 2) StyledRect { radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) topRightRadius: Appearance.rounding.small / 2 bottomRightRadius: Appearance.rounding.small / 2 color: root.disabled ? root.disabledColour : root.colour implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 implicitHeight: expandBtn.implicitHeight StateLayer { id: stateLayer function onClicked(): void { root.active?.clicked(); } rect.topRightRadius: parent.topRightRadius rect.bottomRightRadius: parent.bottomRightRadius color: root.textColour disabled: root.disabled } RowLayout { id: textRow anchors.centerIn: parent anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) spacing: Appearance.spacing.small MaterialIcon { id: iconLabel Layout.alignment: Qt.AlignVCenter animate: true text: root.active?.activeIcon ?? root.fallbackIcon color: root.disabled ? root.disabledTextColour : root.textColour fill: 1 } StyledText { id: label Layout.alignment: Qt.AlignVCenter Layout.preferredWidth: implicitWidth animate: true text: root.active?.activeText ?? root.fallbackText color: root.disabled ? root.disabledTextColour : root.textColour clip: true Behavior on Layout.preferredWidth { Anim { easing.bezierCurve: Appearance.anim.curves.emphasized } } } } } StyledRect { id: expandBtn property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2 radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) topLeftRadius: rad bottomLeftRadius: rad color: root.disabled ? root.disabledColour : root.colour implicitWidth: implicitHeight implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2 StateLayer { id: expandStateLayer function onClicked(): void { root.expanded = !root.expanded; } rect.topLeftRadius: parent.topLeftRadius rect.bottomLeftRadius: parent.bottomLeftRadius color: root.textColour disabled: root.disabled } MaterialIcon { id: expandIcon anchors.centerIn: parent anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4) text: "expand_more" color: root.disabled ? root.disabledTextColour : root.textColour rotation: root.expanded ? 180 : 0 Behavior on anchors.horizontalCenterOffset { Anim {} } Behavior on rotation { Anim {} } } Behavior on rad { Anim {} } Menu { id: menu states: State { when: root.menuOnTop AnchorChanges { target: menu anchors.top: undefined anchors.bottom: expandBtn.top } } anchors.top: parent.bottom anchors.right: parent.right anchors.topMargin: Appearance.spacing.small anchors.bottomMargin: Appearance.spacing.small } } } ================================================ FILE: components/controls/SplitButtonRow.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property string label property int expandedZ: 100 property bool enabled: true property alias menuItems: splitButton.menuItems property alias active: splitButton.active property alias expanded: splitButton.expanded property alias type: splitButton.type signal selected(item: MenuItem) Layout.fillWidth: true implicitHeight: row.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: false z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1 opacity: enabled ? 1.0 : 0.5 RowLayout { id: row anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: root.label color: root.enabled ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant } SplitButton { id: splitButton enabled: root.enabled type: SplitButton.Filled menu.z: 1 stateLayer.onClicked: { splitButton.expanded = !splitButton.expanded; } menu.onItemSelected: item => { root.selected(item); } } } } ================================================ FILE: components/controls/StyledInputField.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.services import qs.config import QtQuick Item { id: root property string text: "" property var validator: null property bool readOnly: false property int horizontalAlignment: TextInput.AlignHCenter property int implicitWidth: 70 property bool enabled: true // Expose activeFocus through alias to avoid FINAL property override readonly property alias hasFocus: inputField.activeFocus signal textEdited(string text) signal editingFinished implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 StyledRect { id: container anchors.fill: parent color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) opacity: root.enabled ? 1 : 0.5 Behavior on color { CAnim {} } Behavior on border.color { CAnim {} } MouseArea { id: inputHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor acceptedButtons: Qt.NoButton enabled: root.enabled } StyledTextField { id: inputField anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: root.horizontalAlignment validator: root.validator readOnly: root.readOnly enabled: root.enabled Binding { target: inputField property: "text" value: root.text when: !inputField.activeFocus } onTextChanged: { root.text = text; root.textEdited(text); } onEditingFinished: { root.editingFinished(); } } } } ================================================ FILE: components/controls/StyledRadioButton.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Templates RadioButton { id: root font.pointSize: Appearance.font.size.smaller implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) indicator: Rectangle { id: outerCircle implicitWidth: 20 implicitHeight: 20 radius: Appearance.rounding.full color: "transparent" border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant border.width: 2 anchors.verticalCenter: parent.verticalCenter StateLayer { function onClicked(): void { root.click(); } anchors.margins: -Appearance.padding.smaller color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary z: -1 } StyledRect { anchors.centerIn: parent implicitWidth: 8 implicitHeight: 8 radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0) } Behavior on border.color { CAnim {} } } contentItem: StyledText { text: root.text font.pointSize: root.font.pointSize anchors.verticalCenter: parent.verticalCenter anchors.left: outerCircle.right anchors.leftMargin: Appearance.spacing.smaller } } ================================================ FILE: components/controls/StyledScrollBar.qml ================================================ import ".." import qs.services import qs.config import QtQuick import QtQuick.Templates ScrollBar { id: root required property Flickable flickable property bool shouldBeActive property real nonAnimPosition property bool animating property bool _updatingFromFlickable: false property bool _updatingFromUser: false onHoveredChanged: { if (hovered) shouldBeActive = true; else shouldBeActive = flickable.moving; } // Sync nonAnimPosition with Qt's automatic position binding onPositionChanged: { if (_updatingFromUser) { _updatingFromUser = false; return; } if (position === nonAnimPosition) { animating = false; return; } if (!animating && !_updatingFromFlickable && !fullMouse.pressed) { nonAnimPosition = position; } } Component.onCompleted: { if (flickable) { const contentHeight = flickable.contentHeight; const height = flickable.height; if (contentHeight > height) { nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); } } } implicitWidth: Appearance.padding.small contentItem: StyledRect { anchors.left: parent.left anchors.right: parent.right opacity: { if (root.size === 1) return 0; if (fullMouse.pressed) return 1; if (mouse.containsMouse) return 0.8; if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive) return 0.6; return 0; } radius: Appearance.rounding.full color: Colours.palette.m3secondary MouseArea { id: mouse anchors.fill: parent cursorShape: Qt.PointingHandCursor hoverEnabled: true acceptedButtons: Qt.NoButton } Behavior on opacity { Anim {} } } // Sync nonAnimPosition with flickable when not animating Connections { function onContentYChanged() { if (!root.animating && !fullMouse.pressed) { root._updatingFromFlickable = true; const contentHeight = root.flickable.contentHeight; const height = root.flickable.height; if (contentHeight > height) { root.nonAnimPosition = Math.max(0, Math.min(1, root.flickable.contentY / (contentHeight - height))); } else { root.nonAnimPosition = 0; } root._updatingFromFlickable = false; } } target: root.flickable } Connections { function onMovingChanged(): void { if (root.flickable.moving) root.shouldBeActive = true; else hideDelay.restart(); } target: root.flickable } Timer { id: hideDelay interval: 600 onTriggered: root.shouldBeActive = root.flickable.moving || root.hovered } CustomMouseArea { id: fullMouse function onWheel(event: WheelEvent): void { root.animating = true; root._updatingFromUser = true; let newPos = root.nonAnimPosition; if (event.angleDelta.y > 0) newPos = Math.max(0, root.nonAnimPosition - 0.1); else if (event.angleDelta.y < 0) newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); root.nonAnimPosition = newPos; // Update flickable position // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] if (root.flickable) { const contentHeight = root.flickable.contentHeight; const height = root.flickable.height; if (contentHeight > height) { const maxContentY = contentHeight - height; const maxPos = 1 - root.size; const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); } } } anchors.fill: parent preventStealing: true onPressed: event => { root.animating = true; root._updatingFromUser = true; const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); root.nonAnimPosition = newPos; // Update flickable position // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] if (root.flickable) { const contentHeight = root.flickable.contentHeight; const height = root.flickable.height; if (contentHeight > height) { const maxContentY = contentHeight - height; const maxPos = 1 - root.size; const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); } } } onPositionChanged: event => { root._updatingFromUser = true; const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); root.nonAnimPosition = newPos; // Update flickable position // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] if (root.flickable) { const contentHeight = root.flickable.contentHeight; const height = root.flickable.height; if (contentHeight > height) { const maxContentY = contentHeight - height; const maxPos = 1 - root.size; const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); } } } } Behavior on position { enabled: !fullMouse.pressed Anim {} } } ================================================ FILE: components/controls/StyledSlider.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Templates Slider { id: root background: Item { StyledRect { anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.topMargin: root.implicitHeight / 3 anchors.bottomMargin: root.implicitHeight / 3 implicitWidth: root.handle.x - root.implicitHeight / 6 color: Colours.palette.m3primary radius: Appearance.rounding.full topRightRadius: root.implicitHeight / 15 bottomRightRadius: root.implicitHeight / 15 } StyledRect { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right anchors.topMargin: root.implicitHeight / 3 anchors.bottomMargin: root.implicitHeight / 3 implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 color: Colours.palette.m3surfaceContainerHighest radius: Appearance.rounding.full topLeftRadius: root.implicitHeight / 15 bottomLeftRadius: root.implicitHeight / 15 } } handle: StyledRect { x: root.visualPosition * root.availableWidth - implicitWidth / 2 implicitWidth: root.implicitHeight / 4.5 implicitHeight: root.implicitHeight color: Colours.palette.m3primary radius: Appearance.rounding.full MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton cursorShape: Qt.PointingHandCursor } } } ================================================ FILE: components/controls/StyledSwitch.qml ================================================ import ".." import qs.services import qs.config import QtQuick import QtQuick.Templates import QtQuick.Shapes Switch { id: root property int cLayer: 1 implicitWidth: implicitIndicatorWidth implicitHeight: implicitIndicatorHeight indicator: StyledRect { radius: Appearance.rounding.full color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) implicitWidth: implicitHeight * 1.7 implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 StyledRect { readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight radius: Appearance.rounding.full color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 implicitWidth: nonAnimWidth implicitHeight: parent.implicitHeight - Appearance.padding.small anchors.verticalCenter: parent.verticalCenter StyledRect { anchors.fill: parent radius: parent.radius color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurface opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 Behavior on opacity { Anim {} } } Shape { id: icon property point start1: { if (root.pressed) return Qt.point(width * 0.2, height / 2); if (root.checked) return Qt.point(width * 0.15, height / 2); return Qt.point(width * 0.15, height * 0.15); } property point end1: { if (root.pressed) { if (root.checked) return Qt.point(width * 0.4, height / 2); return Qt.point(width * 0.8, height / 2); } if (root.checked) return Qt.point(width * 0.4, height * 0.7); return Qt.point(width * 0.85, height * 0.85); } property point start2: { if (root.pressed) { if (root.checked) return Qt.point(width * 0.4, height / 2); return Qt.point(width * 0.2, height / 2); } if (root.checked) return Qt.point(width * 0.4, height * 0.7); return Qt.point(width * 0.15, height * 0.85); } property point end2: { if (root.pressed) return Qt.point(width * 0.8, height / 2); if (root.checked) return Qt.point(width * 0.85, height * 0.2); return Qt.point(width * 0.85, height * 0.15); } anchors.centerIn: parent width: height height: parent.implicitHeight - Appearance.padding.small * 2 preferredRendererType: Shape.CurveRenderer asynchronous: true ShapePath { strokeWidth: Appearance.font.size.larger * 0.15 strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest fillColor: "transparent" capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap startX: icon.start1.x startY: icon.start1.y PathLine { x: icon.end1.x y: icon.end1.y } PathMove { x: icon.start2.x y: icon.start2.y } PathLine { x: icon.end2.x y: icon.end2.y } Behavior on strokeColor { CAnim {} } } Behavior on start1 { PropAnim {} } Behavior on end1 { PropAnim {} } Behavior on start2 { PropAnim {} } Behavior on end2 { PropAnim {} } } Behavior on x { Anim {} } Behavior on implicitWidth { Anim {} } } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor enabled: false } component PropAnim: PropertyAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } } ================================================ FILE: components/controls/StyledTextField.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.services import qs.config import QtQuick import QtQuick.Controls TextField { id: root color: Colours.palette.m3onSurface placeholderTextColor: Colours.palette.m3outline font.family: Appearance.font.family.sans font.pointSize: Appearance.font.size.smaller renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering cursorVisible: !readOnly background: null cursorDelegate: StyledRect { id: cursor property bool disableBlink implicitWidth: 2 color: Colours.palette.m3primary radius: Appearance.rounding.normal Connections { function onCursorPositionChanged(): void { if (root.activeFocus && root.cursorVisible) { cursor.opacity = 1; cursor.disableBlink = true; enableBlink.restart(); } } target: root } Timer { id: enableBlink interval: 100 onTriggered: cursor.disableBlink = false } Timer { running: root.activeFocus && root.cursorVisible && !cursor.disableBlink repeat: true triggeredOnStart: true interval: 500 onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 } Binding { when: !root.activeFocus || !root.cursorVisible cursor.opacity: 0 } Behavior on opacity { Anim { duration: Appearance.anim.durations.small } } } Behavior on color { CAnim {} } Behavior on placeholderTextColor { CAnim {} } } ================================================ FILE: components/controls/SwitchRow.qml ================================================ import ".." import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property string label required property bool checked property bool enabled: true property var onToggled: function (checked) {} Layout.fillWidth: true implicitHeight: row.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: row anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: root.label } StyledSwitch { checked: root.checked enabled: root.enabled onToggled: { root.onToggled(checked); } } } } ================================================ FILE: components/controls/TextButton.qml ================================================ import ".." import qs.services import qs.config import QtQuick StyledRect { id: root enum Type { Filled, Tonal, Text } property alias text: label.text property bool checked property bool toggle property real horizontalPadding: Appearance.padding.normal property real verticalPadding: Appearance.padding.smaller property alias font: label.font property int type: TextButton.Filled property alias stateLayer: stateLayer property alias label: label property bool internalChecked property color activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary property color inactiveColour: { if (!toggle && type === TextButton.Filled) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; } property color activeOnColour: { if (type === TextButton.Text) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary; } property color inactiveOnColour: { if (!toggle && type === TextButton.Filled) return Colours.palette.m3onPrimary; if (type === TextButton.Text) return Colours.palette.m3primary; return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer; } signal clicked onCheckedChanged: internalChecked = checked radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour implicitWidth: label.implicitWidth + horizontalPadding * 2 implicitHeight: label.implicitHeight + verticalPadding * 2 StateLayer { id: stateLayer function onClicked(): void { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } StyledText { id: label anchors.centerIn: parent color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } Behavior on radius { Anim {} } } ================================================ FILE: components/controls/ToggleButton.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property bool toggled property string icon property string label property string accent: "Secondary" property real iconSize: Appearance.font.size.large property real horizontalPadding: Appearance.padding.large property real verticalPadding: Appearance.padding.normal property string tooltip: "" property bool hovered: false signal clicked Component.onCompleted: { hovered = toggleStateLayer.containsMouse; } Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] Connections { function onContainsMouseChanged() { const newHovered = toggleStateLayer.containsMouse; if (root.hovered !== newHovered) { root.hovered = newHovered; } } target: toggleStateLayer } StateLayer { id: toggleStateLayer function onClicked(): void { root.clicked(); } color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] } RowLayout { id: toggleBtnInner anchors.centerIn: parent spacing: Appearance.spacing.normal MaterialIcon { id: toggleBtnIcon visible: !!text fill: root.toggled ? 1 : 0 text: root.icon color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] font.pointSize: root.iconSize Behavior on fill { Anim {} } } Loader { asynchronous: true active: !!root.label visible: active sourceComponent: StyledText { text: root.label color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] } } } Behavior on radius { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Behavior on Layout.preferredWidth { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } // Tooltip - positioned absolutely, doesn't affect layout Loader { id: tooltipLoader asynchronous: true active: root.tooltip !== "" z: 10000 width: 0 height: 0 sourceComponent: Component { Tooltip { target: root text: root.tooltip } } // Completely remove from layout Layout.fillWidth: false Layout.fillHeight: false Layout.preferredWidth: 0 Layout.preferredHeight: 0 Layout.maximumWidth: 0 Layout.maximumHeight: 0 Layout.minimumWidth: 0 Layout.minimumHeight: 0 } } ================================================ FILE: components/controls/ToggleRow.qml ================================================ import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts RowLayout { id: root required property string label property alias checked: toggle.checked property alias toggle: toggle Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: root.label } StyledSwitch { id: toggle cLayer: 2 } } ================================================ FILE: components/controls/Tooltip.qml ================================================ import ".." import qs.components.effects import qs.services import qs.config import QtQuick import QtQuick.Controls Popup { id: root required property Item target required property string text property int delay: 500 property int timeout: 0 property bool tooltipVisible: false property Timer showTimer: Timer { interval: root.delay onTriggered: root.tooltipVisible = true } property Timer hideTimer: Timer { interval: root.timeout onTriggered: root.tooltipVisible = false } function updatePosition() { if (!target || !parent) return; // Wait for tooltipRect to have its size calculated Qt.callLater(() => { if (!target || !parent || !tooltipRect) return; // Get target position in parent's coordinate system const targetPos = target.mapToItem(parent, 0, 0); const targetCenterX = targetPos.x + target.width / 2; // Get tooltip size (use width/height if available, otherwise implicit) const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth; const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight; // Center tooltip horizontally on target let newX = targetCenterX - tooltipWidth / 2; // Position tooltip above target let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; // Keep within bounds const padding = Appearance.padding.normal; if (newX < padding) { newX = padding; } else if (newX + tooltipWidth > (parent.width - padding)) { newX = parent.width - tooltipWidth - padding; } // Update popup position x = newX; y = newY; }); } // Popup properties - doesn't affect layout parent: { let p = target; // Walk up to find the root Item (usually has anchors.fill: parent) while (p && p.parent) { const parentItem = p.parent; // Check if this looks like a root pane Item if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { return parentItem; } p = parentItem; } // Fallback return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; } visible: tooltipVisible modal: false closePolicy: Popup.NoAutoClose padding: 0 margins: 0 background: Item {} // Update position when target moves or tooltip becomes visible onTooltipVisibleChanged: { if (tooltipVisible) { Qt.callLater(updatePosition); } } Component.onCompleted: { if (tooltipVisible) { updatePosition(); } } enter: Transition { Anim { property: "opacity" from: 0 to: 1 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } exit: Transition { Anim { property: "opacity" from: 1 to: 0 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } contentItem: StyledRect { id: tooltipRect implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 color: Colours.palette.m3surfaceContainerHighest radius: Appearance.rounding.small antialiasing: true // Add elevation for depth Elevation { anchors.fill: parent radius: parent.radius z: -1 level: 3 } StyledText { id: tooltipText anchors.centerIn: parent text: root.text color: Colours.palette.m3onSurface font.pointSize: Appearance.font.size.small } } Connections { function onXChanged() { if (root.tooltipVisible) root.updatePosition(); } function onYChanged() { if (root.tooltipVisible) root.updatePosition(); } function onWidthChanged() { if (root.tooltipVisible) root.updatePosition(); } function onHeightChanged() { if (root.tooltipVisible) root.updatePosition(); } target: root.target } // Monitor hover state Connections { function onHoveredChanged() { if (target.hovered) { showTimer.start(); if (timeout > 0) { hideTimer.stop(); hideTimer.start(); } } else { showTimer.stop(); hideTimer.stop(); tooltipVisible = false; } } target: root.target } } ================================================ FILE: components/effects/ColouredIcon.qml ================================================ pragma ComponentBehavior: Bound import Caelestia import Quickshell.Widgets import QtQuick IconImage { id: root required property color colour asynchronous: true layer.enabled: true layer.effect: Colouriser { sourceColor: analyser.dominantColour colorizationColor: root.colour } layer.onEnabledChanged: { if (layer.enabled && status === Image.Ready) analyser.requestUpdate(); } onStatusChanged: { if (layer.enabled && status === Image.Ready) analyser.requestUpdate(); } ImageAnalyser { id: analyser sourceItem: root } } ================================================ FILE: components/effects/Colouriser.qml ================================================ import ".." import QtQuick import QtQuick.Effects MultiEffect { property color sourceColor: "black" colorization: 1 brightness: 1 - sourceColor.hslLightness Behavior on colorizationColor { CAnim {} } } ================================================ FILE: components/effects/Elevation.qml ================================================ import ".." import qs.services import QtQuick import QtQuick.Effects RectangularShadow { property int level property real dp: [0, 1, 3, 6, 8, 12][level] color: Qt.alpha(Colours.palette.m3shadow, 0.7) blur: (dp * 5) ** 0.7 spread: -dp * 0.3 + (dp * 0.1) ** 2 offset.y: dp / 2 Behavior on dp { Anim {} } } ================================================ FILE: components/effects/InnerBorder.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.services import qs.config import QtQuick import QtQuick.Effects StyledRect { property alias innerRadius: maskInner.radius property alias thickness: maskInner.anchors.margins property alias leftThickness: maskInner.anchors.leftMargin property alias topThickness: maskInner.anchors.topMargin property alias rightThickness: maskInner.anchors.rightMargin property alias bottomThickness: maskInner.anchors.bottomMargin anchors.fill: parent color: Colours.tPalette.m3surfaceContainer layer.enabled: true layer.effect: MultiEffect { maskSource: mask maskEnabled: true maskInverted: true maskThresholdMin: 0.5 maskSpreadAtMin: 1 } Item { id: mask anchors.fill: parent layer.enabled: true visible: false Rectangle { id: maskInner anchors.fill: parent anchors.margins: Appearance.padding.normal radius: Appearance.rounding.small } } } ================================================ FILE: components/effects/OpacityMask.qml ================================================ import Quickshell import QtQuick ShaderEffect { required property Item source required property Item maskSource fragmentShader: Quickshell.shellPath("assets/shaders/opacitymask.frag.qsb") } ================================================ FILE: components/filedialog/CurrentItem.qml ================================================ import ".." import qs.services import qs.config import QtQuick import QtQuick.Shapes Item { id: root required property var currentItem implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 Shape { preferredRendererType: Shape.CurveRenderer ShapePath { id: path readonly property real rounding: Appearance.rounding.small readonly property bool flatten: root.implicitHeight < rounding * 2 readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding strokeWidth: -1 fillColor: Colours.tPalette.m3surfaceContainer startX: root.implicitWidth startY: root.implicitHeight PathLine { relativeX: -(root.implicitWidth + path.rounding) relativeY: 0 } PathArc { relativeX: path.rounding relativeY: -path.roundingY radiusX: path.rounding radiusY: Math.min(path.rounding, root.implicitHeight) direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: -(root.implicitHeight - path.roundingY * 2) } PathArc { relativeX: path.rounding relativeY: -path.roundingY radiusX: path.rounding radiusY: Math.min(path.rounding, root.implicitHeight) } PathLine { relativeX: root.implicitHeight > 0 ? root.implicitWidth - path.rounding * 2 : root.implicitWidth relativeY: 0 } PathArc { relativeX: path.rounding relativeY: -path.rounding radiusX: path.rounding radiusY: path.rounding direction: PathArc.Counterclockwise } Behavior on fillColor { CAnim {} } } } Item { anchors.fill: parent clip: true StyledText { id: content anchors.right: parent.right anchors.bottom: parent.bottom anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small Connections { function onCurrentItemChanged(): void { if (root.currentItem) content.text = qsTr(`"%1" selected`).arg(root.currentItem.modelData.name); } target: root } } } Behavior on implicitWidth { enabled: !!root.currentItem Anim {} } Behavior on implicitHeight { Anim {} } } ================================================ FILE: components/filedialog/DialogButtons.qml ================================================ import qs.components import qs.services import qs.config import QtQuick.Layouts StyledRect { id: root required property var dialog required property FolderContents folder implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer RowLayout { id: inner anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.small StyledText { text: qsTr("Filter:") } StyledRect { Layout.fillWidth: true Layout.fillHeight: true Layout.rightMargin: Appearance.spacing.normal color: Colours.tPalette.m3surfaceContainerHigh radius: Appearance.rounding.small StyledText { anchors.fill: parent anchors.margins: Appearance.padding.normal text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` } } StyledRect { color: Colours.tPalette.m3surfaceContainerHigh radius: Appearance.rounding.small implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { root.dialog.accepted(root.folder.currentItem.modelData.path); } disabled: !root.dialog.selectionValid } StyledText { id: selectText anchors.centerIn: parent anchors.margins: Appearance.padding.normal text: qsTr("Select") color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline } } StyledRect { color: Colours.tPalette.m3surfaceContainerHigh radius: Appearance.rounding.small implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { root.dialog.rejected(); } } StyledText { id: cancelText anchors.centerIn: parent anchors.margins: Appearance.padding.normal text: qsTr("Cancel") } } } } ================================================ FILE: components/filedialog/FileDialog.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import Quickshell import QtQuick import QtQuick.Layouts LazyLoader { id: loader property list cwd: ["Home"] property string filterLabel: "All files" property list filters: ["*"] property string title: qsTr("Select a file") signal accepted(path: string) signal rejected function open(): void { activeAsync = true; } function close(): void { rejected(); } onAccepted: activeAsync = false onRejected: activeAsync = false FloatingWindow { id: root property list cwd: loader.cwd property string filterLabel: loader.filterLabel property list filters: loader.filters readonly property bool selectionValid: { const file = folderContents.currentItem?.modelData; return (file && !file.isDir && (filters.includes("*") || filters.includes(file.suffix))) ?? false; } function accepted(path: string): void { loader.accepted(path); } function rejected(): void { loader.rejected(); } implicitWidth: 1000 implicitHeight: 600 color: Colours.tPalette.m3surface title: loader.title onVisibleChanged: { if (!visible) rejected(); } RowLayout { anchors.fill: parent spacing: 0 Sidebar { Layout.fillHeight: true dialog: root } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 0 HeaderBar { Layout.fillWidth: true dialog: root } FolderContents { id: folderContents Layout.fillWidth: true Layout.fillHeight: true dialog: root } DialogButtons { Layout.fillWidth: true dialog: root folder: folderContents } } } Behavior on color { CAnim {} } } } ================================================ FILE: components/filedialog/FolderContents.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.filedialog import qs.components.controls import qs.components.images import qs.services import qs.config import qs.utils import Caelestia.Models import Quickshell import QtQuick import QtQuick.Layouts import QtQuick.Effects Item { id: root required property var dialog readonly property FileEntry currentItem: view.currentItem as FileEntry StyledRect { anchors.fill: parent color: Colours.tPalette.m3surfaceContainer layer.enabled: true layer.effect: MultiEffect { maskSource: mask maskEnabled: true maskInverted: true maskThresholdMin: 0.5 maskSpreadAtMin: 1 } } Item { id: mask anchors.fill: parent layer.enabled: true visible: false Rectangle { anchors.fill: parent anchors.margins: Appearance.padding.small radius: Appearance.rounding.small } } Loader { asynchronous: true anchors.centerIn: parent opacity: view.count === 0 ? 1 : 0 active: opacity > 0 sourceComponent: ColumnLayout { MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.extraLarge * 2 font.weight: 500 } StyledText { text: qsTr("This folder is empty") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.large font.weight: 500 } } Behavior on opacity { Anim {} } } GridView { id: view anchors.fill: parent anchors.margins: Appearance.padding.small + Appearance.padding.normal cellWidth: Sizes.itemWidth + Appearance.spacing.small cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 clip: true focus: true currentIndex: -1 Keys.onEscapePressed: currentIndex = -1 Keys.onReturnPressed: { if (root.dialog.selectionValid) root.dialog.accepted((currentItem as FileEntry).modelData.path); } Keys.onEnterPressed: { if (root.dialog.selectionValid) root.dialog.accepted((currentItem as FileEntry).modelData.path); } StyledScrollBar.vertical: StyledScrollBar { flickable: view } model: FileSystemModel { path: { if (root.dialog.cwd[0] === "Home") return Paths.home + `/${root.dialog.cwd.slice(1).join("/")}`; else return root.dialog.cwd.join("/"); } onPathChanged: view.currentIndex = -1 } delegate: FileEntry {} add: Transition { Anim { properties: "opacity,scale" from: 0 to: 1 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } remove: Transition { Anim { property: "opacity" to: 0 } Anim { property: "scale" to: 0.5 } } displaced: Transition { Anim { properties: "opacity,scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { properties: "x,y" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } CurrentItem { anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Appearance.padding.small currentItem: view.currentItem } component FileEntry: StyledRect { id: item required property int index required property FileSystemEntry modelData readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 implicitWidth: Sizes.itemWidth implicitHeight: nonAnimHeight radius: Appearance.rounding.normal color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 clip: true StateLayer { function onClicked(): void { view.currentIndex = item.index; } onDoubleClicked: { if (item.modelData.isDir) root.dialog.cwd.push(item.modelData.name); else if (root.dialog.selectionValid) root.dialog.accepted(item.modelData.path); } } CachingIconImage { id: icon anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: Appearance.padding.normal implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 Component.onCompleted: { const file = item.modelData; if (file.isImage) source = Qt.resolvedUrl(file.path); else if (!file.isDir) source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); else source = Quickshell.iconPath("inode-directory"); } } StyledText { id: name anchors.left: parent.left anchors.right: parent.right anchors.top: icon.bottom anchors.topMargin: Appearance.spacing.small anchors.margins: Appearance.padding.normal horizontalAlignment: Text.AlignHCenter elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap Component.onCompleted: text = item.modelData.name } Behavior on implicitHeight { Anim {} } } } ================================================ FILE: components/filedialog/HeaderBar.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property var dialog implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer RowLayout { id: inner anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.small Item { implicitWidth: implicitHeight implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 StateLayer { function onClicked(): void { root.dialog.cwd.pop(); } radius: Appearance.rounding.small disabled: root.dialog.cwd.length === 1 } MaterialIcon { id: upIcon anchors.centerIn: parent text: "drive_folder_upload" color: root.dialog.cwd.length === 1 ? Colours.palette.m3outline : Colours.palette.m3onSurface grade: 200 } } StyledRect { Layout.fillWidth: true radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainerHigh implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 RowLayout { id: pathComponents anchors.fill: parent anchors.margins: Appearance.padding.small / 2 anchors.leftMargin: 0 spacing: Appearance.spacing.small Repeater { model: root.dialog.cwd RowLayout { id: folder required property string modelData required property int index spacing: 0 Loader { asynchronous: true Layout.rightMargin: Appearance.spacing.small active: folder.index > 0 sourceComponent: StyledText { text: "/" color: Colours.palette.m3onSurfaceVariant font.bold: true } } Item { implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 Loader { asynchronous: true anchors.fill: parent active: folder.index < root.dialog.cwd.length - 1 sourceComponent: StateLayer { function onClicked(): void { root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); } radius: Appearance.rounding.small } } Loader { id: homeIcon asynchronous: true anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.padding.normal active: folder.index === 0 && folder.modelData === "Home" sourceComponent: MaterialIcon { text: "home" color: root.dialog.cwd.length === 1 ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant fill: 1 } } StyledText { id: folderName anchors.left: homeIcon.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 text: folder.modelData color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface font.bold: true } } } } Item { Layout.fillWidth: true } } } } } ================================================ FILE: components/filedialog/Sidebar.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.filedialog import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property var dialog implicitWidth: Sizes.sidebarWidth implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: inner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.small / 2 StyledText { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.padding.small / 2 Layout.bottomMargin: Appearance.spacing.normal text: qsTr("Files") color: Colours.palette.m3onSurface font.pointSize: Appearance.font.size.larger font.bold: true } Repeater { model: ["Home", "Downloads", "Desktop", "Documents", "Music", "Pictures", "Videos"] StyledRect { id: place required property string modelData readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] Layout.fillWidth: true implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) StateLayer { function onClicked(): void { if (place.modelData === "Home") root.dialog.cwd = ["Home"]; else root.dialog.cwd = ["Home", place.modelData]; } color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } RowLayout { id: placeInner anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large spacing: Appearance.spacing.normal MaterialIcon { text: { const p = place.modelData; if (p === "Home") return "home"; if (p === "Downloads") return "file_download"; if (p === "Desktop") return "desktop_windows"; if (p === "Documents") return "description"; if (p === "Music") return "music_note"; if (p === "Pictures") return "image"; if (p === "Videos") return "video_library"; return "folder"; } color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.large fill: place.selected ? 1 : 0 Behavior on fill { Anim {} } } StyledText { Layout.fillWidth: true text: place.modelData color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } } } } } } ================================================ FILE: components/filedialog/Sizes.qml ================================================ pragma Singleton import Quickshell Singleton { property int itemWidth: 103 property int sidebarWidth: 200 } ================================================ FILE: components/images/CachingIconImage.qml ================================================ pragma ComponentBehavior: Bound import qs.utils import Quickshell.Widgets import QtQuick Item { id: root // Easier (and more efficient) to ignore it than to check type and cast readonly property int status: loader.item?.status ?? Image.Null // qmllint disable missing-property readonly property real actualSize: Math.min(width, height) property real implicitSize property url source implicitWidth: implicitSize implicitHeight: implicitSize Loader { id: loader asynchronous: true anchors.fill: parent sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null } Component { id: cachingImage CachingImage { path: Paths.toLocalFile(root.source) fillMode: Image.PreserveAspectFit } } Component { id: iconImage IconImage { source: root.source asynchronous: true } } } ================================================ FILE: components/images/CachingImage.qml ================================================ import qs.utils import Caelestia.Internal import Quickshell import QtQuick Image { id: root property alias path: manager.path asynchronous: true fillMode: Image.PreserveAspectCrop Connections { function onDevicePixelRatioChanged(): void { manager.updateSource(); } target: QsWindow.window } CachingImageManager { id: manager item: root cacheDir: Qt.resolvedUrl(Paths.imagecache) } } ================================================ FILE: components/misc/CustomShortcut.qml ================================================ import Quickshell.Hyprland GlobalShortcut { appid: "caelestia" } ================================================ FILE: components/misc/Ref.qml ================================================ import QtQuick QtObject { required property var service Component.onCompleted: service.refCount++ Component.onDestruction: service.refCount-- } ================================================ FILE: components/widgets/ExtraIndicator.qml ================================================ import ".." import "../effects" import qs.services import qs.config import QtQuick StyledRect { required property int extra anchors.right: parent.right anchors.margins: Appearance.padding.normal color: Colours.palette.m3tertiary radius: Appearance.rounding.small implicitWidth: count.implicitWidth + Appearance.padding.normal * 2 implicitHeight: count.implicitHeight + Appearance.padding.small * 2 opacity: extra > 0 ? 1 : 0 scale: extra > 0 ? 1 : 0.5 Elevation { anchors.fill: parent radius: parent.radius opacity: parent.opacity z: -1 level: 2 } StyledText { id: count anchors.centerIn: parent animate: parent.opacity > 0 text: qsTr("+%1").arg(parent.extra) color: Colours.palette.m3onTertiary } Behavior on opacity { Anim { duration: Appearance.anim.durations.expressiveFastSpatial } } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } ================================================ FILE: config/Appearance.qml ================================================ pragma Singleton import Quickshell Singleton { // Literally just here to shorten accessing stuff :woe: // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx` readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing readonly property AppearanceConfig.Padding padding: Config.appearance.padding readonly property AppearanceConfig.FontStuff font: Config.appearance.font readonly property AppearanceConfig.Anim anim: Config.appearance.anim readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency } ================================================ FILE: config/AppearanceConfig.qml ================================================ import Quickshell.Io JsonObject { property Rounding rounding: Rounding {} property Spacing spacing: Spacing {} property Padding padding: Padding {} property FontStuff font: FontStuff {} property Anim anim: Anim {} property Transparency transparency: Transparency {} component Rounding: JsonObject { property real scale: 1 property int small: 12 * scale property int normal: 17 * scale property int large: 25 * scale property int full: 1000 * scale } component Spacing: JsonObject { property real scale: 1 property int small: 7 * scale property int smaller: 10 * scale property int normal: 12 * scale property int larger: 15 * scale property int large: 20 * scale } component Padding: JsonObject { property real scale: 1 property int small: 5 * scale property int smaller: 7 * scale property int normal: 10 * scale property int larger: 12 * scale property int large: 15 * scale } component FontFamily: JsonObject { property string sans: "Rubik" property string mono: "CaskaydiaCove NF" property string material: "Material Symbols Rounded" property string clock: "Rubik" } component FontSize: JsonObject { property real scale: 1 property int small: 11 * scale property int smaller: 12 * scale property int normal: 13 * scale property int larger: 15 * scale property int large: 18 * scale property int extraLarge: 28 * scale } component FontStuff: JsonObject { property FontFamily family: FontFamily {} property FontSize size: FontSize {} } component AnimCurves: JsonObject { property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] property list standard: [0.2, 0, 0, 1, 1, 1] property list standardAccel: [0.3, 0, 1, 1, 1, 1] property list standardDecel: [0, 0, 0, 1, 1, 1] property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] } component AnimDurations: JsonObject { property real scale: 1 property int small: 200 * scale property int normal: 400 * scale property int large: 600 * scale property int extraLarge: 1000 * scale property int expressiveFastSpatial: 350 * scale property int expressiveDefaultSpatial: 500 * scale property int expressiveEffects: 200 * scale } component Anim: JsonObject { property real mediaGifSpeedAdjustment: 300 property real sessionGifSpeed: 0.7 property AnimCurves curves: AnimCurves {} property AnimDurations durations: AnimDurations {} } component Transparency: JsonObject { property bool enabled: false property real base: 0.85 property real layers: 0.4 } } ================================================ FILE: config/BackgroundConfig.qml ================================================ import Quickshell.Io JsonObject { property bool enabled: true property bool wallpaperEnabled: true property DesktopClock desktopClock: DesktopClock {} property Visualiser visualiser: Visualiser {} component DesktopClock: JsonObject { property bool enabled: false property real scale: 1.0 property string position: "bottom-right" property bool invertColors: false property DesktopClockBackground background: DesktopClockBackground {} property DesktopClockShadow shadow: DesktopClockShadow {} } component DesktopClockBackground: JsonObject { property bool enabled: false property real opacity: 0.7 property bool blur: true } component DesktopClockShadow: JsonObject { property bool enabled: true property real opacity: 0.7 property real blur: 0.4 } component Visualiser: JsonObject { property bool enabled: false property bool autoHide: true property bool blur: false property real rounding: 1 property real spacing: 1 } } ================================================ FILE: config/BarConfig.qml ================================================ import Quickshell.Io JsonObject { property bool persistent: true property bool showOnHover: true property int dragThreshold: 20 property ScrollActions scrollActions: ScrollActions {} property Popouts popouts: Popouts {} property Workspaces workspaces: Workspaces {} property ActiveWindow activeWindow: ActiveWindow {} property Tray tray: Tray {} property Status status: Status {} property Clock clock: Clock {} property Sizes sizes: Sizes {} property list excludedScreens: [] property list entries: [ { id: "logo", enabled: true }, { id: "workspaces", enabled: true }, { id: "spacer", enabled: true }, { id: "activeWindow", enabled: true }, { id: "spacer", enabled: true }, { id: "tray", enabled: true }, { id: "clock", enabled: true }, { id: "statusIcons", enabled: true }, { id: "power", enabled: true } ] component ScrollActions: JsonObject { property bool workspaces: true property bool volume: true property bool brightness: true } component Popouts: JsonObject { property bool activeWindow: true property bool tray: true property bool statusIcons: true } component Workspaces: JsonObject { property int shown: 5 property bool activeIndicator: true property bool occupiedBg: false property bool showWindows: true property bool showWindowsOnSpecialWorkspaces: showWindows property int maxWindowIcons: 0 // 0 = unlimited property bool activeTrail: false property bool perMonitorWorkspaces: true property string label: " " // if empty, will show workspace name's first letter property string occupiedLabel: "󰮯" property string activeLabel: "󰮯" property string capitalisation: "preserve" // upper, lower, or preserve - relevant only if label is empty property list specialWorkspaceIcons: [] property list windowIcons: [ { regex: "steam(_app_(default|[0-9]+))?", icon: "sports_esports" } ] } component ActiveWindow: JsonObject { property bool compact: false property bool inverted: false property bool showOnHover: true } component Tray: JsonObject { property bool background: false property bool recolour: false property bool compact: false property list iconSubs: [] property list hiddenIcons: [] } component Status: JsonObject { property bool showAudio: false property bool showMicrophone: false property bool showKbLayout: false property bool showNetwork: true property bool showWifi: true property bool showBluetooth: true property bool showBattery: true property bool showLockStatus: true } component Clock: JsonObject { property bool background: false property bool showDate: false property bool showIcon: true } component Sizes: JsonObject { property int innerWidth: 40 property int windowPreviewSize: 400 property int trayMenuWidth: 300 property int batteryWidth: 250 property int networkWidth: 320 property int kbLayoutWidth: 320 } } ================================================ FILE: config/BorderConfig.qml ================================================ import Quickshell.Io import qs.config JsonObject { property int thickness: Config.appearance.padding.normal property int rounding: Config.appearance.rounding.large readonly property int minThickness: 2 readonly property int clampedThickness: Math.max(minThickness, thickness) } ================================================ FILE: config/Config.qml ================================================ pragma Singleton import qs.utils import Caelestia import Quickshell import Quickshell.Io import QtQuick Singleton { id: root property alias appearance: adapter.appearance property alias general: adapter.general property alias background: adapter.background property alias bar: adapter.bar property alias border: adapter.border property alias dashboard: adapter.dashboard property alias controlCenter: adapter.controlCenter property alias launcher: adapter.launcher property alias notifs: adapter.notifs property alias osd: adapter.osd property alias session: adapter.session property alias winfo: adapter.winfo property alias lock: adapter.lock property alias utilities: adapter.utilities property alias sidebar: adapter.sidebar property alias services: adapter.services property alias paths: adapter.paths property bool recentlySaved: false // Public save function - call this to persist config changes function save(): void { saveTimer.restart(); recentlySaved = true; recentSaveCooldown.restart(); } // Helper function to serialize the config object function serializeConfig(): var { return { appearance: serializeAppearance(), general: serializeGeneral(), background: serializeBackground(), bar: serializeBar(), border: serializeBorder(), dashboard: serializeDashboard(), controlCenter: serializeControlCenter(), launcher: serializeLauncher(), notifs: serializeNotifs(), osd: serializeOsd(), session: serializeSession(), winfo: serializeWinfo(), lock: serializeLock(), utilities: serializeUtilities(), sidebar: serializeSidebar(), services: serializeServices(), paths: serializePaths() }; } function serializeAppearance(): var { return { rounding: { scale: appearance.rounding.scale }, spacing: { scale: appearance.spacing.scale }, padding: { scale: appearance.padding.scale }, font: { family: { sans: appearance.font.family.sans, mono: appearance.font.family.mono, material: appearance.font.family.material, clock: appearance.font.family.clock }, size: { scale: appearance.font.size.scale } }, anim: { mediaGifSpeedAdjustment: 300, sessionGifSpeed: 0.7, durations: { scale: appearance.anim.durations.scale } }, transparency: { enabled: appearance.transparency.enabled, base: appearance.transparency.base, layers: appearance.transparency.layers } }; } function serializeGeneral(): var { return { logo: general.logo, excludedScreens: general.excludedScreens, apps: { terminal: general.apps.terminal, audio: general.apps.audio, playback: general.apps.playback, explorer: general.apps.explorer }, idle: { lockBeforeSleep: general.idle.lockBeforeSleep, inhibitWhenAudio: general.idle.inhibitWhenAudio, timeouts: general.idle.timeouts }, battery: { warnLevels: general.battery.warnLevels, criticalLevel: general.battery.criticalLevel } }; } function serializeBackground(): var { return { enabled: background.enabled, wallpaperEnabled: background.wallpaperEnabled, desktopClock: { enabled: background.desktopClock.enabled, scale: background.desktopClock.scale, position: background.desktopClock.position, invertColors: background.desktopClock.invertColors, background: { enabled: background.desktopClock.background.enabled, opacity: background.desktopClock.background.opacity, blur: background.desktopClock.background.blur }, shadow: { enabled: background.desktopClock.shadow.enabled, opacity: background.desktopClock.shadow.opacity, blur: background.desktopClock.shadow.blur } }, visualiser: { enabled: background.visualiser.enabled, autoHide: background.visualiser.autoHide, blur: background.visualiser.blur, rounding: background.visualiser.rounding, spacing: background.visualiser.spacing } }; } function serializeBar(): var { return { persistent: bar.persistent, showOnHover: bar.showOnHover, dragThreshold: bar.dragThreshold, scrollActions: { workspaces: bar.scrollActions.workspaces, volume: bar.scrollActions.volume, brightness: bar.scrollActions.brightness }, popouts: { activeWindow: bar.popouts.activeWindow, tray: bar.popouts.tray, statusIcons: bar.popouts.statusIcons }, workspaces: { shown: bar.workspaces.shown, activeIndicator: bar.workspaces.activeIndicator, occupiedBg: bar.workspaces.occupiedBg, showWindows: bar.workspaces.showWindows, showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, maxWindowIcons: bar.workspaces.maxWindowIcons, activeTrail: bar.workspaces.activeTrail, perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, label: bar.workspaces.label, occupiedLabel: bar.workspaces.occupiedLabel, activeLabel: bar.workspaces.activeLabel, capitalisation: bar.workspaces.capitalisation, specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons, windowIcons: bar.workspaces.windowIcons }, activeWindow: { compact: bar.activeWindow.compact, inverted: bar.activeWindow.inverted, showOnHover: bar.activeWindow.showOnHover }, tray: { background: bar.tray.background, recolour: bar.tray.recolour, compact: bar.tray.compact, iconSubs: bar.tray.iconSubs, hiddenIcons: bar.tray.hiddenIcons }, status: { showAudio: bar.status.showAudio, showMicrophone: bar.status.showMicrophone, showKbLayout: bar.status.showKbLayout, showNetwork: bar.status.showNetwork, showWifi: bar.status.showWifi, showBluetooth: bar.status.showBluetooth, showBattery: bar.status.showBattery, showLockStatus: bar.status.showLockStatus }, clock: { background: bar.clock.background, showDate: bar.clock.showDate, showIcon: bar.clock.showIcon }, entries: bar.entries, excludedScreens: bar.excludedScreens }; } function serializeBorder(): var { return { thickness: border.thickness, rounding: border.rounding }; } function serializeDashboard(): var { return { enabled: dashboard.enabled, showOnHover: dashboard.showOnHover, mediaUpdateInterval: dashboard.mediaUpdateInterval, resourceUpdateInterval: dashboard.resourceUpdateInterval, dragThreshold: dashboard.dragThreshold, performance: { showBattery: dashboard.performance.showBattery, showGpu: dashboard.performance.showGpu, showCpu: dashboard.performance.showCpu, showMemory: dashboard.performance.showMemory, showStorage: dashboard.performance.showStorage, showNetwork: dashboard.performance.showNetwork } }; } function serializeControlCenter(): var { return {}; } function serializeLauncher(): var { return { enabled: launcher.enabled, showOnHover: launcher.showOnHover, maxShown: launcher.maxShown, maxWallpapers: launcher.maxWallpapers, specialPrefix: launcher.specialPrefix, actionPrefix: launcher.actionPrefix, enableDangerousActions: launcher.enableDangerousActions, dragThreshold: launcher.dragThreshold, vimKeybinds: launcher.vimKeybinds, favouriteApps: launcher.favouriteApps, hiddenApps: launcher.hiddenApps, useFuzzy: { apps: launcher.useFuzzy.apps, actions: launcher.useFuzzy.actions, schemes: launcher.useFuzzy.schemes, variants: launcher.useFuzzy.variants, wallpapers: launcher.useFuzzy.wallpapers }, actions: launcher.actions }; } function serializeNotifs(): var { return { expire: notifs.expire, defaultExpireTimeout: notifs.defaultExpireTimeout, clearThreshold: notifs.clearThreshold, expandThreshold: notifs.expandThreshold, actionOnClick: notifs.actionOnClick, groupPreviewNum: notifs.groupPreviewNum }; } function serializeOsd(): var { return { enabled: osd.enabled, hideDelay: osd.hideDelay, enableBrightness: osd.enableBrightness, enableMicrophone: osd.enableMicrophone }; } function serializeSession(): var { return { enabled: session.enabled, dragThreshold: session.dragThreshold, vimKeybinds: session.vimKeybinds, icons: { logout: session.icons.logout, shutdown: session.icons.shutdown, hibernate: session.icons.hibernate, reboot: session.icons.reboot }, commands: { logout: session.commands.logout, shutdown: session.commands.shutdown, hibernate: session.commands.hibernate, reboot: session.commands.reboot } }; } function serializeWinfo(): var { return {}; } function serializeLock(): var { return { recolourLogo: lock.recolourLogo, enableFprint: lock.enableFprint, maxFprintTries: lock.maxFprintTries, hideNotifs: lock.hideNotifs }; } function serializeUtilities(): var { return { enabled: utilities.enabled, maxToasts: utilities.maxToasts, toasts: { configLoaded: utilities.toasts.configLoaded, chargingChanged: utilities.toasts.chargingChanged, gameModeChanged: utilities.toasts.gameModeChanged, dndChanged: utilities.toasts.dndChanged, audioOutputChanged: utilities.toasts.audioOutputChanged, audioInputChanged: utilities.toasts.audioInputChanged, capsLockChanged: utilities.toasts.capsLockChanged, numLockChanged: utilities.toasts.numLockChanged, kbLayoutChanged: utilities.toasts.kbLayoutChanged, vpnChanged: utilities.toasts.vpnChanged, nowPlaying: utilities.toasts.nowPlaying }, vpn: { enabled: utilities.vpn.enabled, provider: utilities.vpn.provider }, quickToggles: utilities.quickToggles }; } function serializeSidebar(): var { return { enabled: sidebar.enabled, dragThreshold: sidebar.dragThreshold }; } function serializeServices(): var { return { weatherLocation: services.weatherLocation, useFahrenheit: services.useFahrenheit, useFahrenheitPerformance: services.useFahrenheitPerformance, useTwelveHourClock: services.useTwelveHourClock, gpuType: services.gpuType, visualiserBars: services.visualiserBars, audioIncrement: services.audioIncrement, brightnessIncrement: services.brightnessIncrement, maxVolume: services.maxVolume, smartScheme: services.smartScheme, defaultPlayer: services.defaultPlayer, playerAliases: services.playerAliases, showLyrics: services.showLyrics }; } function serializePaths(): var { return { wallpaperDir: paths.wallpaperDir, lyricsDir: paths.lyricsDir, sessionGif: paths.sessionGif, mediaGif: paths.mediaGif }; } ElapsedTimer { id: timer } Timer { id: saveTimer interval: 500 onTriggered: { timer.restart(); try { // Parse current config to preserve structure and comments if possible let config = {}; try { config = JSON.parse(fileView.text()); } catch (e) { // If parsing fails, start with empty object config = {}; } // Update config with current values config = root.serializeConfig(); // Save to file with pretty printing fileView.setText(JSON.stringify(config, null, 2)); } catch (e) { Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); } } } Timer { id: recentSaveCooldown interval: 2000 onTriggered: { root.recentlySaved = false; } } FileView { id: fileView path: `${Paths.config}/shell.json` watchChanges: true onFileChanged: { // Prevent reload loop - don't reload if we just saved if (!root.recentlySaved) { timer.restart(); reload(); } else { // Self-initiated save - reload without toast reload(); } } onLoaded: { try { JSON.parse(text()); const elapsed = timer.elapsedMs(); // Only show toast for external changes (not our own saves) and when elapsed time is meaningful if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) { Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) { Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); } } catch (e) { Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); } } onLoadFailed: err => { if (err !== FileViewError.FileNotFound) Toaster.toast(qsTr("Failed to read config file"), FileViewError.toString(err), "settings_alert", Toast.Warning); } onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) JsonAdapter { id: adapter property AppearanceConfig appearance: AppearanceConfig {} property GeneralConfig general: GeneralConfig {} property BackgroundConfig background: BackgroundConfig {} property BarConfig bar: BarConfig {} property BorderConfig border: BorderConfig {} property DashboardConfig dashboard: DashboardConfig {} property ControlCenterConfig controlCenter: ControlCenterConfig {} property LauncherConfig launcher: LauncherConfig {} property NotifsConfig notifs: NotifsConfig {} property OsdConfig osd: OsdConfig {} property SessionConfig session: SessionConfig {} property WInfoConfig winfo: WInfoConfig {} property LockConfig lock: LockConfig {} property UtilitiesConfig utilities: UtilitiesConfig {} property SidebarConfig sidebar: SidebarConfig {} property ServiceConfig services: ServiceConfig {} property UserPaths paths: UserPaths {} } } } ================================================ FILE: config/ControlCenterConfig.qml ================================================ import Quickshell.Io JsonObject { property Sizes sizes: Sizes {} component Sizes: JsonObject { property real heightMult: 0.7 property real ratio: 16 / 9 } } ================================================ FILE: config/DashboardConfig.qml ================================================ import Quickshell.Io JsonObject { property bool enabled: true property bool showOnHover: true property int mediaUpdateInterval: 500 property int resourceUpdateInterval: 1000 property int dragThreshold: 50 property bool showDashboard: true property bool showMedia: true property bool showPerformance: true property bool showWeather: true property Sizes sizes: Sizes {} property Performance performance: Performance {} component Performance: JsonObject { property bool showBattery: true property bool showGpu: true property bool showCpu: true property bool showMemory: true property bool showStorage: true property bool showNetwork: true } component Sizes: JsonObject { readonly property int tabIndicatorHeight: 3 readonly property int tabIndicatorSpacing: 5 readonly property int infoWidth: 200 readonly property int infoIconSize: 25 readonly property int dateTimeWidth: 110 readonly property int mediaWidth: 200 readonly property int mediaProgressSweep: 180 readonly property int mediaProgressThickness: 8 readonly property int resourceProgessThickness: 10 readonly property int weatherWidth: 250 readonly property int mediaCoverArtSize: 150 readonly property int mediaVisualiserSize: 80 readonly property int resourceSize: 200 } } ================================================ FILE: config/GeneralConfig.qml ================================================ import Quickshell.Io JsonObject { property string logo: "" property list excludedScreens: [] property Apps apps: Apps {} property Idle idle: Idle {} property Battery battery: Battery {} component Apps: JsonObject { property list terminal: ["foot"] property list audio: ["pavucontrol"] property list playback: ["mpv"] property list explorer: ["thunar"] } component Idle: JsonObject { property bool lockBeforeSleep: true property bool inhibitWhenAudio: true property list timeouts: [ { timeout: 180, idleAction: "lock" }, { timeout: 300, idleAction: "dpms off", returnAction: "dpms on" }, { timeout: 600, idleAction: ["systemctl", "suspend-then-hibernate"] } ] } component Battery: JsonObject { property list warnLevels: [ { level: 20, title: qsTr("Low battery"), message: qsTr("You might want to plug in a charger"), icon: "battery_android_frame_2" }, { level: 10, title: qsTr("Did you see the previous message?"), message: qsTr("You should probably plug in a charger now"), icon: "battery_android_frame_1" }, { level: 5, title: qsTr("Critical battery level"), message: qsTr("PLUG THE CHARGER RIGHT NOW!!"), icon: "battery_android_alert", critical: true }, ] property int criticalLevel: 3 } } ================================================ FILE: config/LauncherConfig.qml ================================================ import Quickshell.Io JsonObject { property bool enabled: true property bool showOnHover: false property int maxShown: 7 property int maxWallpapers: 9 // Warning: even numbers look bad property string specialPrefix: "@" property string actionPrefix: ">" property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout property int dragThreshold: 50 property bool vimKeybinds: false property list favouriteApps: [] property list hiddenApps: [] property UseFuzzy useFuzzy: UseFuzzy {} property Sizes sizes: Sizes {} component UseFuzzy: JsonObject { property bool apps: false property bool actions: false property bool schemes: false property bool variants: false property bool wallpapers: false } component Sizes: JsonObject { property int itemWidth: 600 property int itemHeight: 57 property int wallpaperWidth: 280 property int wallpaperHeight: 200 } property list actions: [ { name: "Calculator", icon: "calculate", description: "Do simple math equations (powered by Qalc)", command: ["autocomplete", "calc"], enabled: true, dangerous: false }, { name: "Scheme", icon: "palette", description: "Change the current colour scheme", command: ["autocomplete", "scheme"], enabled: true, dangerous: false }, { name: "Wallpaper", icon: "image", description: "Change the current wallpaper", command: ["autocomplete", "wallpaper"], enabled: true, dangerous: false }, { name: "Variant", icon: "colors", description: "Change the current scheme variant", command: ["autocomplete", "variant"], enabled: true, dangerous: false }, { name: "Transparency", icon: "opacity", description: "Change shell transparency", command: ["autocomplete", "transparency"], enabled: false, dangerous: false }, { name: "Random", icon: "casino", description: "Switch to a random wallpaper", command: ["caelestia", "wallpaper", "-r"], enabled: true, dangerous: false }, { name: "Light", icon: "light_mode", description: "Change the scheme to light mode", command: ["setMode", "light"], enabled: true, dangerous: false }, { name: "Dark", icon: "dark_mode", description: "Change the scheme to dark mode", command: ["setMode", "dark"], enabled: true, dangerous: false }, { name: "Shutdown", icon: "power_settings_new", description: "Shutdown the system", command: ["systemctl", "poweroff"], enabled: true, dangerous: true }, { name: "Reboot", icon: "cached", description: "Reboot the system", command: ["systemctl", "reboot"], enabled: true, dangerous: true }, { name: "Logout", icon: "exit_to_app", description: "Log out of the current session", command: ["loginctl", "terminate-user", ""], enabled: true, dangerous: true }, { name: "Lock", icon: "lock", description: "Lock the current session", command: ["loginctl", "lock-session"], enabled: true, dangerous: false }, { name: "Sleep", icon: "bedtime", description: "Suspend then hibernate", command: ["systemctl", "suspend-then-hibernate"], enabled: true, dangerous: false }, { name: "Settings", icon: "settings", description: "Configure the shell", command: ["caelestia", "shell", "controlCenter", "open"], enabled: true, dangerous: false } ] } ================================================ FILE: config/LockConfig.qml ================================================ import Quickshell.Io JsonObject { property bool recolourLogo: false property bool enableFprint: true property int maxFprintTries: 3 property bool hideNotifs: false property Sizes sizes: Sizes {} component Sizes: JsonObject { property real heightMult: 0.7 property real ratio: 16 / 9 property int centerWidth: 600 } } ================================================ FILE: config/NotifsConfig.qml ================================================ import Quickshell.Io JsonObject { property bool expire: true property int defaultExpireTimeout: 5000 property real clearThreshold: 0.3 property int expandThreshold: 20 property bool actionOnClick: false property int groupPreviewNum: 3 property bool openExpanded: false // Show the notifichation in expanded state when opening property Sizes sizes: Sizes {} component Sizes: JsonObject { property int width: 400 property int image: 41 property int badge: 20 } } ================================================ FILE: config/OsdConfig.qml ================================================ import Quickshell.Io JsonObject { property bool enabled: true property int hideDelay: 2000 property bool enableBrightness: true property bool enableMicrophone: false property Sizes sizes: Sizes {} component Sizes: JsonObject { property int sliderWidth: 30 property int sliderHeight: 150 } } ================================================ FILE: config/ServiceConfig.qml ================================================ import Quickshell.Io import QtQuick JsonObject { property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) property bool useFahrenheitPerformance: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") property string gpuType: "" property int visualiserBars: 45 property real audioIncrement: 0.1 property real brightnessIncrement: 0.1 property real maxVolume: 1.0 property bool smartScheme: true property string defaultPlayer: "Spotify" property list playerAliases: [ { "from": "com.github.th_ch.youtube_music", "to": "YT Music" } ] property bool showLyrics: true } ================================================ FILE: config/SessionConfig.qml ================================================ import Quickshell.Io JsonObject { property bool enabled: true property int dragThreshold: 30 property bool vimKeybinds: false property Icons icons: Icons {} property Commands commands: Commands {} property Sizes sizes: Sizes {} component Icons: JsonObject { property string logout: "logout" property string shutdown: "power_settings_new" property string hibernate: "downloading" property string reboot: "cached" } component Commands: JsonObject { property list logout: ["loginctl", "terminate-user", ""] property list shutdown: ["systemctl", "poweroff"] property list hibernate: ["systemctl", "hibernate"] property list reboot: ["systemctl", "reboot"] } component Sizes: JsonObject { property int button: 80 } } ================================================ FILE: config/SidebarConfig.qml ================================================ import Quickshell.Io JsonObject { property bool enabled: true property int dragThreshold: 80 property Sizes sizes: Sizes {} component Sizes: JsonObject { property int width: 430 } } ================================================ FILE: config/UserPaths.qml ================================================ import qs.utils import Quickshell.Io JsonObject { property string wallpaperDir: `${Paths.pictures}/Wallpapers` property string lyricsDir: `${Paths.home}/Music/lyrics/` property string sessionGif: "root:/assets/kurukuru.gif" property string mediaGif: "root:/assets/bongocat.gif" } ================================================ FILE: config/UtilitiesConfig.qml ================================================ import Quickshell.Io JsonObject { property bool enabled: true property int maxToasts: 4 property Sizes sizes: Sizes {} property Toasts toasts: Toasts {} property Vpn vpn: Vpn {} component Sizes: JsonObject { property int width: 430 property int toastWidth: 430 } component Toasts: JsonObject { property bool configLoaded: true property bool chargingChanged: true property bool gameModeChanged: true property bool dndChanged: true property bool audioOutputChanged: true property bool audioInputChanged: true property bool capsLockChanged: true property bool numLockChanged: true property bool kbLayoutChanged: true property bool kbLimit: true property bool vpnChanged: true property bool nowPlaying: false } component Vpn: JsonObject { property bool enabled: false property list provider: ["netbird"] } property list quickToggles: [ { id: "wifi", enabled: true }, { id: "bluetooth", enabled: true }, { id: "mic", enabled: true }, { id: "settings", enabled: true }, { id: "gameMode", enabled: true }, { id: "dnd", enabled: true }, { id: "vpn", enabled: false } ] } ================================================ FILE: config/WInfoConfig.qml ================================================ import Quickshell.Io JsonObject { property Sizes sizes: Sizes {} component Sizes: JsonObject { property real heightMult: 0.7 property real detailsWidth: 500 } } ================================================ FILE: extras/CMakeLists.txt ================================================ # Version add_executable(version version.cpp) target_compile_definitions(version PRIVATE PROJECT_NAME="${PROJECT_NAME}" VERSION="${VERSION}" GIT_REVISION="${GIT_REVISION}" DISTRIBUTOR="${DISTRIBUTOR}" ) install(TARGETS version DESTINATION ${INSTALL_LIBDIR}) ================================================ FILE: extras/version.cpp ================================================ #include int main(int argc, char* argv[]) { if (argc > 1) { std::string arg = argv[1]; if (arg == "-t" || arg == "--terse") { std::cout << PROJECT_NAME << std::endl; std::cout << VERSION << std::endl; std::cout << GIT_REVISION << std::endl; std::cout << DISTRIBUTOR << std::endl; } else if (arg == "-s" || arg == "--short") { std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION << ", distributed by: " << DISTRIBUTOR << std::endl; } else { std::cout << "Usage: " << argv[0] << " [-t | --terse] [-s | --short]" << std::endl; return arg != "-h" && arg != "--help"; } } else { std::cout << "Project: " << PROJECT_NAME << std::endl; std::cout << "Version: " << VERSION << std::endl; std::cout << "Git revision: " << GIT_REVISION << std::endl; std::cout << "Distributor: " << DISTRIBUTOR << std::endl; } return 0; } ================================================ FILE: flake.nix ================================================ { description = "Desktop shell for Caelestia dots"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; quickshell = { url = "git+https://git.outfoxxed.me/outfoxxed/quickshell"; inputs.nixpkgs.follows = "nixpkgs"; }; caelestia-cli = { url = "github:caelestia-dots/cli"; inputs.nixpkgs.follows = "nixpkgs"; inputs.caelestia-shell.follows = ""; }; }; outputs = { self, nixpkgs, ... } @ inputs: let forAllSystems = fn: nixpkgs.lib.genAttrs nixpkgs.lib.platforms.linux ( system: fn nixpkgs.legacyPackages.${system} ); in { formatter = forAllSystems (pkgs: pkgs.alejandra); packages = forAllSystems (pkgs: rec { caelestia-shell = pkgs.callPackage ./nix { rev = self.rev or self.dirtyRev; stdenv = pkgs.clangStdenv; quickshell = inputs.quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default.override { withX11 = false; withI3 = false; }; caelestia-cli = inputs.caelestia-cli.packages.${pkgs.stdenv.hostPlatform.system}.default; }; with-cli = caelestia-shell.override {withCli = true;}; debug = caelestia-shell.override {debug = true;}; default = caelestia-shell; }); devShells = forAllSystems (pkgs: { default = let shell = self.packages.${pkgs.stdenv.hostPlatform.system}.caelestia-shell; in pkgs.mkShell.override {stdenv = shell.stdenv;} { inputsFrom = [shell shell.plugin shell.extras]; packages = with pkgs; [clazy material-symbols rubik nerd-fonts.caskaydia-cove]; CAELESTIA_XKB_RULES_PATH = "${pkgs.xkeyboard-config}/share/xkeyboard-config-2/rules/base.lst"; }; }); homeManagerModules.default = import ./nix/hm-module.nix self; }; } ================================================ FILE: modules/BatteryMonitor.qml ================================================ import qs.config import Caelestia import Quickshell import Quickshell.Services.UPower import QtQuick Scope { id: root readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) Connections { function onOnBatteryChanged(): void { if (UPower.onBattery) { if (Config.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); } else { if (Config.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); for (const level of root.warnLevels) level.warned = false; } } target: UPower } Connections { function onPercentageChanged(): void { if (!UPower.onBattery) return; const p = UPower.displayDevice.percentage * 100; for (const level of root.warnLevels) { if (p <= level.level && !level.warned) { level.warned = true; 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); } } if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) { Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error); hibernateTimer.start(); } } target: UPower.displayDevice } Timer { id: hibernateTimer interval: 5000 onTriggered: Quickshell.execDetached(["systemctl", "hibernate"]) } } ================================================ FILE: modules/IdleMonitors.qml ================================================ pragma ComponentBehavior: Bound import "lock" import qs.config import qs.services import Caelestia.Internal import Quickshell import Quickshell.Wayland Scope { id: root required property Lock lock readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) function handleIdleAction(action: var): void { if (!action) return; if (action === "lock") lock.lock.locked = true; else if (action === "unlock") lock.lock.locked = false; else if (typeof action === "string") Hypr.dispatch(action); else Quickshell.execDetached(action); } LogindManager { onAboutToSleep: { if (Config.general.idle.lockBeforeSleep) root.lock.lock.locked = true; } onLockRequested: root.lock.lock.locked = true onUnlockRequested: root.lock.lock.unlock() } Variants { model: Config.general.idle.timeouts IdleMonitor { required property var modelData enabled: root.enabled && (modelData.enabled ?? true) timeout: modelData.timeout respectInhibitors: modelData.respectInhibitors ?? true onIsIdleChanged: root.handleIdleAction(isIdle ? modelData.idleAction : modelData.returnAction) } } } ================================================ FILE: modules/Shortcuts.qml ================================================ import qs.components.misc import qs.modules.controlcenter import qs.services import Caelestia import Quickshell import Quickshell.Io Scope { id: root property bool launcherInterrupted readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false CustomShortcut { name: "controlCenter" description: "Open control center" onPressed: WindowFactory.create() } CustomShortcut { name: "showall" description: "Toggle launcher, dashboard and osd" onPressed: { if (root.hasFullscreen) return; const v = Visibilities.getForActive(); v.launcher = v.dashboard = v.osd = v.utilities = !(v.launcher || v.dashboard || v.osd || v.utilities); } } CustomShortcut { name: "dashboard" description: "Toggle dashboard" onPressed: { if (root.hasFullscreen) return; const visibilities = Visibilities.getForActive(); visibilities.dashboard = !visibilities.dashboard; } } CustomShortcut { name: "session" description: "Toggle session menu" onPressed: { if (root.hasFullscreen) return; const visibilities = Visibilities.getForActive(); visibilities.session = !visibilities.session; } } CustomShortcut { name: "launcher" description: "Toggle launcher" onPressed: root.launcherInterrupted = false onReleased: { if (!root.launcherInterrupted && !root.hasFullscreen) { const visibilities = Visibilities.getForActive(); visibilities.launcher = !visibilities.launcher; } root.launcherInterrupted = false; } } CustomShortcut { name: "launcherInterrupt" description: "Interrupt launcher keybind" onPressed: root.launcherInterrupted = true } CustomShortcut { name: "sidebar" description: "Toggle sidebar" onPressed: { if (root.hasFullscreen) return; const visibilities = Visibilities.getForActive(); visibilities.sidebar = !visibilities.sidebar; } } CustomShortcut { name: "utilities" description: "Toggle utilities" onPressed: { if (root.hasFullscreen) return; const visibilities = Visibilities.getForActive(); visibilities.utilities = !visibilities.utilities; } } IpcHandler { function toggle(drawer: string): void { if (list().split("\n").includes(drawer)) { if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer)) return; const visibilities = Visibilities.getForActive(); visibilities[drawer] = !visibilities[drawer]; } else { console.warn(`[IPC] Drawer "${drawer}" does not exist`); } } function list(): string { const visibilities = Visibilities.getForActive(); return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n"); } target: "drawers" } IpcHandler { function open(): void { WindowFactory.create(); } target: "controlCenter" } IpcHandler { function info(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Info); } function success(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Success); } function warn(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Warning); } function error(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Error); } target: "toaster" } } ================================================ FILE: modules/areapicker/AreaPicker.qml ================================================ pragma ComponentBehavior: Bound import qs.components.containers import qs.components.misc import qs.services import Quickshell import Quickshell.Wayland import Quickshell.Io Scope { LazyLoader { id: root property bool freeze property bool closing property bool clipboardOnly Variants { model: Screens.screens StyledWindow { id: win required property ShellScreen modelData screen: modelData name: "area-picker" WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive mask: root.closing ? empty : null anchors.top: true anchors.bottom: true anchors.left: true anchors.right: true Region { id: empty } Picker { loader: root screen: win.modelData } } } } IpcHandler { function open(): void { root.freeze = false; root.closing = false; root.clipboardOnly = false; root.activeAsync = true; } function openFreeze(): void { root.freeze = true; root.closing = false; root.clipboardOnly = false; root.activeAsync = true; } function openClip(): void { root.freeze = false; root.closing = false; root.clipboardOnly = true; root.activeAsync = true; } function openFreezeClip(): void { root.freeze = true; root.closing = false; root.clipboardOnly = true; root.activeAsync = true; } target: "picker" } CustomShortcut { name: "screenshot" description: "Open screenshot tool" onPressed: { root.freeze = false; root.closing = false; root.clipboardOnly = false; root.activeAsync = true; } } CustomShortcut { name: "screenshotFreeze" description: "Open screenshot tool (freeze mode)" onPressed: { root.freeze = true; root.closing = false; root.clipboardOnly = false; root.activeAsync = true; } } CustomShortcut { name: "screenshotClip" description: "Open screenshot tool (clipboard)" onPressed: { root.freeze = false; root.closing = false; root.clipboardOnly = true; root.activeAsync = true; } } CustomShortcut { name: "screenshotFreezeClip" description: "Open screenshot tool (freeze mode, clipboard)" onPressed: { root.freeze = true; root.closing = false; root.clipboardOnly = true; root.activeAsync = true; } } } ================================================ FILE: modules/areapicker/Picker.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Caelestia import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick import QtQuick.Effects MouseArea { id: root required property LazyLoader loader required property ShellScreen screen property bool onClient property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2 property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0 property real ssx property real ssy property real sx: 0 property real sy: 0 property real ex: screen.width property real ey: screen.height property real rsx: Math.min(sx, ex) property real rsy: Math.min(sy, ey) property real sw: Math.abs(sx - ex) property real sh: Math.abs(sy - ey) property list clients: { const mon = Hypr.monitorFor(screen); if (!mon) return []; const special = mon.lastIpcObject.specialWorkspace; const wsId = special.name ? special.id : mon.activeWorkspace.id; return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => { // Pinned first, then fullscreen, then floating, then any other const ac = a.lastIpcObject; const bc = b.lastIpcObject; return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating); }); } function checkClientRects(x: real, y: real): void { for (const client of clients) { if (!client) continue; let { at: [cx, cy], size: [cw, ch] } = client.lastIpcObject; cx -= screen.x; cy -= screen.y; if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) { onClient = true; sx = cx; sy = cy; ex = cx + cw; ey = cy + ch; break; } } } function save(): void { const tmpfile = Qt.resolvedUrl(`/tmp/caelestia-picker-${Quickshell.processId}-${Date.now()}.png`); CUtils.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => { if (root.loader.clipboardOnly) { Quickshell.execDetached(["sh", "-c", "wl-copy --type image/png < " + path]); Quickshell.execDetached(["notify-send", "-a", "caelestia-cli", "-i", path, "Screenshot taken", "Screenshot copied to clipboard"]); } else { Quickshell.execDetached(["swappy", "-f", path]); } closeAnim.start(); }); } onClientsChanged: checkClientRects(mouseX, mouseY) anchors.fill: parent opacity: 0 hoverEnabled: true cursorShape: Qt.CrossCursor Component.onCompleted: { Hypr.extras.refreshOptions(); // Break binding if frozen if (loader.freeze) clients = clients; opacity = 1; const c = clients[0]; if (c) { const cx = c.lastIpcObject.at[0] - screen.x; const cy = c.lastIpcObject.at[1] - screen.y; onClient = true; sx = cx; sy = cy; ex = cx + c.lastIpcObject.size[0]; ey = cy + c.lastIpcObject.size[1]; } else { sx = screen.width / 2 - 100; sy = screen.height / 2 - 100; ex = screen.width / 2 + 100; ey = screen.height / 2 + 100; } } onPressed: event => { ssx = event.x; ssy = event.y; } onReleased: { if (closeAnim.running) return; if (root.loader.freeze) { save(); } else { overlay.visible = border.visible = false; screencopy.visible = false; screencopy.active = true; } } onPositionChanged: event => { const x = event.x; const y = event.y; if (pressed) { onClient = false; sx = ssx; sy = ssy; ex = x; ey = y; } else { checkClientRects(x, y); } } focus: true Keys.onEscapePressed: closeAnim.start() SequentialAnimation { id: closeAnim PropertyAction { target: root.loader property: "closing" value: true } ParallelAnimation { Anim { target: root property: "opacity" to: 0 duration: Appearance.anim.durations.large } ExAnim { target: root properties: "rsx,rsy" to: 0 } ExAnim { target: root property: "sw" to: root.screen.width } ExAnim { target: root property: "sh" to: root.screen.height } } PropertyAction { target: root.loader property: "activeAsync" value: false } } Process { running: true command: ["hyprctl", "cursorpos", "-j"] stdout: StdioCollector { onStreamFinished: { const pos = JSON.parse(text); root.checkClientRects(pos.x - root.screen.x, pos.y - root.screen.y); } } } Loader { id: screencopy asynchronous: true anchors.fill: parent active: root.loader.freeze sourceComponent: ScreencopyView { captureSource: root.screen onHasContentChanged: { if (hasContent && !root.loader.freeze) { overlay.visible = border.visible = true; root.save(); } } } } StyledRect { id: overlay anchors.fill: parent color: Colours.palette.m3secondaryContainer opacity: 0.3 layer.enabled: true layer.effect: MultiEffect { maskSource: selectionWrapper maskEnabled: true maskInverted: true maskSpreadAtMin: 1 maskThresholdMin: 0.5 } } Item { id: selectionWrapper anchors.fill: parent layer.enabled: true visible: false Rectangle { id: selectionRect radius: root.realRounding x: root.rsx y: root.rsy implicitWidth: root.sw implicitHeight: root.sh } } Rectangle { id: border color: "transparent" radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 border.width: root.realBorderWidth border.color: Colours.palette.m3primary x: selectionRect.x - root.realBorderWidth y: selectionRect.y - root.realBorderWidth implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 Behavior on border.color { CAnim {} } } Behavior on opacity { Anim { duration: Appearance.anim.durations.large } } Behavior on rsx { enabled: !root.pressed ExAnim {} } Behavior on rsy { enabled: !root.pressed ExAnim {} } Behavior on sw { enabled: !root.pressed ExAnim {} } Behavior on sh { enabled: !root.pressed ExAnim {} } component ExAnim: Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } ================================================ FILE: modules/background/Background.qml ================================================ pragma ComponentBehavior: Bound import qs.components.containers import qs.services import qs.config import Quickshell import Quickshell.Wayland import QtQuick Loader { asynchronous: true active: Config.background.enabled sourceComponent: Variants { model: Screens.screens StyledWindow { id: win required property ShellScreen modelData screen: modelData name: "background" WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom color: Config.background.wallpaperEnabled ? "black" : "transparent" surfaceFormat.opaque: false anchors.top: true anchors.bottom: true anchors.left: true anchors.right: true Item { id: behindClock anchors.fill: parent Loader { id: wallpaper asynchronous: true anchors.fill: parent active: Config.background.wallpaperEnabled sourceComponent: Wallpaper {} } Visualiser { anchors.fill: parent screen: win.modelData wallpaper: wallpaper } } Loader { id: clockLoader asynchronous: true active: Config.background.desktopClock.enabled anchors.margins: Appearance.padding.large * 2 anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) state: Config.background.desktopClock.position states: [ State { name: "top-left" AnchorChanges { target: clockLoader anchors.top: parent.top anchors.left: parent.left } }, State { name: "top-center" AnchorChanges { target: clockLoader anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter } }, State { name: "top-right" AnchorChanges { target: clockLoader anchors.top: parent.top anchors.right: parent.right } }, State { name: "middle-left" AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left } }, State { name: "middle-center" AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter } }, State { name: "middle-right" AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right } }, State { name: "bottom-left" AnchorChanges { target: clockLoader anchors.bottom: parent.bottom anchors.left: parent.left } }, State { name: "bottom-center" AnchorChanges { target: clockLoader anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter } }, State { name: "bottom-right" AnchorChanges { target: clockLoader anchors.bottom: parent.bottom anchors.right: parent.right } } ] transitions: Transition { AnchorAnimation { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } sourceComponent: DesktopClock { wallpaper: behindClock absX: clockLoader.x absY: clockLoader.y } } } } } ================================================ FILE: modules/background/DesktopClock.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts import QtQuick.Effects Item { id: root required property Item wallpaper required property real absX required property real absY property real scale: Config.background.desktopClock.scale readonly property bool bgEnabled: Config.background.desktopClock.background.enabled readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled readonly property bool invertColors: Config.background.desktopClock.invertColors readonly property bool useLightSet: Colours.light ? !invertColors : invertColors readonly property color safePrimary: useLightSet ? Colours.palette.m3primaryContainer : Colours.palette.m3primary readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.scale) implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.scale) Item { id: clockContainer anchors.fill: parent layer.enabled: Config.background.desktopClock.shadow.enabled layer.effect: MultiEffect { shadowEnabled: true shadowColor: Colours.palette.m3shadow shadowOpacity: Config.background.desktopClock.shadow.opacity shadowBlur: Config.background.desktopClock.shadow.blur } Loader { asynchronous: true anchors.fill: parent active: root.blurEnabled sourceComponent: MultiEffect { source: ShaderEffectSource { sourceItem: root.wallpaper sourceRect: Qt.rect(root.absX, root.absY, root.width, root.height) } maskSource: backgroundPlate maskEnabled: true blurEnabled: true blur: 1 blurMax: 64 autoPaddingEnabled: false } } StyledRect { id: backgroundPlate visible: root.bgEnabled anchors.fill: parent radius: Appearance.rounding.large * root.scale opacity: Config.background.desktopClock.background.opacity color: Colours.palette.m3surface layer.enabled: root.blurEnabled } RowLayout { id: layout anchors.centerIn: parent spacing: Appearance.spacing.larger * root.scale RowLayout { spacing: Appearance.spacing.small StyledText { text: Time.hourStr font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale font.weight: Font.Bold color: root.safePrimary } StyledText { text: ":" font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale color: root.safeTertiary opacity: 0.8 Layout.topMargin: -Appearance.padding.large * 1.5 * root.scale } StyledText { text: Time.minuteStr font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale font.weight: Font.Bold color: root.safeSecondary } Loader { asynchronous: true Layout.alignment: Qt.AlignTop Layout.topMargin: Appearance.padding.large * 1.4 * root.scale active: Config.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr font.pointSize: Appearance.font.size.large * root.scale color: root.safeSecondary } } } StyledRect { Layout.fillHeight: true Layout.preferredWidth: 4 * root.scale Layout.topMargin: Appearance.spacing.larger * root.scale Layout.bottomMargin: Appearance.spacing.larger * root.scale radius: Appearance.rounding.full color: root.safePrimary opacity: 0.8 } ColumnLayout { spacing: 0 StyledText { text: Time.format("MMMM").toUpperCase() font.pointSize: Appearance.font.size.large * root.scale font.letterSpacing: 4 font.weight: Font.Bold color: root.safeSecondary } StyledText { text: Time.format("dd") font.pointSize: Appearance.font.size.extraLarge * root.scale font.letterSpacing: 2 font.weight: Font.Medium color: root.safePrimary } StyledText { text: Time.format("dddd") font.pointSize: Appearance.font.size.larger * root.scale font.letterSpacing: 2 color: root.safeSecondary } } } } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Behavior on implicitWidth { Anim { duration: Appearance.anim.durations.small } } } ================================================ FILE: modules/background/Visualiser.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Caelestia.Services import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Effects Item { id: root required property ShellScreen screen required property Item wallpaper readonly property bool shouldBeActive: Config.background.visualiser.enabled && (!Config.background.visualiser.autoHide || (Hypr.monitorFor(screen)?.activeWorkspace?.toplevels?.values.every(t => t.lastIpcObject?.floating) ?? true)) property real offset: shouldBeActive ? 0 : screen.height * 0.2 opacity: shouldBeActive ? 1 : 0 Loader { asynchronous: true anchors.fill: parent active: root.opacity > 0 && Config.background.visualiser.blur sourceComponent: MultiEffect { source: root.wallpaper maskSource: wrapper maskEnabled: true blurEnabled: true blur: 1 blurMax: 32 autoPaddingEnabled: false } } Item { id: wrapper anchors.fill: parent layer.enabled: true Loader { asynchronous: true anchors.fill: parent anchors.topMargin: root.offset anchors.bottomMargin: -root.offset active: root.opacity > 0 sourceComponent: Item { ServiceRef { service: Audio.cava } Item { id: content anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing Side { content: content } Side { content: content isRight: true } Behavior on anchors.leftMargin { Anim {} } } } } } Behavior on offset { Anim {} } Behavior on opacity { Anim {} } component Side: Repeater { id: side required property Item content property bool isRight model: Config.services.visualiserBars ClippingRectangle { id: bar required property int modelData property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1])) clip: true x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0) implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing y: side.content.height - height implicitHeight: bar.value * side.content.height * 0.4 color: "transparent" topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding Rectangle { topLeftRadius: parent.topLeftRadius topRightRadius: parent.topRightRadius gradient: Gradient { orientation: Gradient.Vertical GradientStop { position: 0 color: Qt.alpha(Colours.palette.m3primary, 0.7) Behavior on color { CAnim {} } } GradientStop { position: 1 color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) Behavior on color { CAnim {} } } } anchors.left: parent.left anchors.right: parent.right y: parent.height - height implicitHeight: side.content.height * 0.4 } Behavior on value { Anim { duration: Appearance.anim.durations.small } } } } } ================================================ FILE: modules/background/Wallpaper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.images import qs.components.filedialog import qs.services import qs.config import qs.utils import QtQuick Item { id: root property string source: Wallpapers.current property Image current: one property bool completed onSourceChanged: { if (!source) current = null; else if (current === one) two.update(); else one.update(); } Component.onCompleted: { if (source) Qt.callLater(() => { one.update(); completed = true; }); } Loader { asynchronous: true anchors.fill: parent active: root.completed && !root.source sourceComponent: StyledRect { color: Colours.palette.m3surfaceContainer Row { anchors.centerIn: parent spacing: Appearance.spacing.large MaterialIcon { text: "sentiment_stressed" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.extraLarge * 5 } Column { anchors.verticalCenter: parent.verticalCenter spacing: Appearance.spacing.small StyledText { text: qsTr("Wallpaper missing?") color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.extraLarge * 2 font.bold: true } StyledRect { implicitWidth: selectWallText.implicitWidth + Appearance.padding.large * 2 implicitHeight: selectWallText.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.full color: Colours.palette.m3primary FileDialog { id: dialog title: qsTr("Select a wallpaper") filterLabel: qsTr("Image files") filters: Images.validImageExtensions onAccepted: path => Wallpapers.setWallpaper(path) } StateLayer { function onClicked(): void { dialog.open(); } radius: parent.radius color: Colours.palette.m3onPrimary } StyledText { id: selectWallText anchors.centerIn: parent text: qsTr("Set it now!") color: Colours.palette.m3onPrimary font.pointSize: Appearance.font.size.large } } } } } } Img { id: one } Img { id: two } component Img: CachingImage { id: img function update(): void { if (path === root.source) root.current = this; else path = root.source; } anchors.fill: parent opacity: 0 scale: Wallpapers.showPreview ? 1 : 0.8 onStatusChanged: { if (status === Image.Ready) root.current = this; } states: State { name: "visible" when: root.current === img PropertyChanges { img.opacity: 1 img.scale: 1 } } transitions: Transition { Anim { target: img properties: "opacity,scale" } } } } ================================================ FILE: modules/bar/Bar.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import "popouts" as BarPopouts import "components" import "components/workspaces" import Quickshell import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property ShellScreen screen required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts readonly property int vPadding: Appearance.padding.large function closeTray(): void { if (!Config.bar.tray.compact) return; for (let i = 0; i < repeater.count; i++) { const loader = repeater.itemAt(i) as WrappedLoader; if (loader?.enabled && loader.id === "tray") { (loader.item as Tray).expanded = false; } } } function checkPopout(y: real): void { const ch = childAt(width / 2, y) as WrappedLoader; if (ch?.id !== "tray") closeTray(); if (!ch) { popouts.hasCurrent = false; return; } const id = ch.id; const top = ch.y; if (id === "statusIcons" && Config.bar.popouts.statusIcons) { const items = (ch.item as StatusIcons).items; const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); if (icon) { popouts.currentName = icon.name; popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, 0, icon.implicitHeight / 2).y); popouts.hasCurrent = true; } } else if (id === "tray" && Config.bar.popouts.tray) { const tray = ch.item as Tray; if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); const trayItem = tray.items.itemAt(index); if (trayItem) { popouts.currentName = `traymenu${index}`; popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); popouts.hasCurrent = true; } else { popouts.hasCurrent = false; } } else { popouts.hasCurrent = false; tray.expanded = true; } } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { popouts.currentName = id.toLowerCase(); popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; popouts.hasCurrent = true; } } function handleWheel(y: real, angleDelta: point): void { const ch = childAt(width / 2, y) as WrappedLoader; if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) { // Workspace scroll const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); const specialWs = mon?.lastIpcObject.specialWorkspace.name; if (specialWs?.length > 0) Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`); else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`); } else if (y < screen.height / 2 && Config.bar.scrollActions.volume) { // Volume scroll on top half if (angleDelta.y > 0) Audio.incrementVolume(); else if (angleDelta.y < 0) Audio.decrementVolume(); } else if (Config.bar.scrollActions.brightness) { // Brightness scroll on bottom half const monitor = Brightness.getMonitorForScreen(screen); if (angleDelta.y > 0) monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); else if (angleDelta.y < 0) monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); } } spacing: Appearance.spacing.normal Repeater { id: repeater model: Config.bar.entries DelegateChooser { role: "id" DelegateChoice { roleValue: "spacer" delegate: WrappedLoader { Layout.fillHeight: enabled } } DelegateChoice { roleValue: "logo" delegate: WrappedLoader { sourceComponent: OsIcon {} } } DelegateChoice { roleValue: "workspaces" delegate: WrappedLoader { sourceComponent: Workspaces { screen: root.screen } } } DelegateChoice { roleValue: "activeWindow" delegate: WrappedLoader { Layout.fillWidth: true sourceComponent: ActiveWindow { bar: root monitor: Brightness.getMonitorForScreen(root.screen) } } } DelegateChoice { roleValue: "tray" delegate: WrappedLoader { sourceComponent: Tray {} } } DelegateChoice { roleValue: "clock" delegate: WrappedLoader { sourceComponent: Clock {} } } DelegateChoice { roleValue: "statusIcons" delegate: WrappedLoader { sourceComponent: StatusIcons {} } } DelegateChoice { roleValue: "power" delegate: WrappedLoader { sourceComponent: Power { visibilities: root.visibilities } } } } } component WrappedLoader: Loader { required property bool enabled required property string id required property int index function findFirstEnabled(): Item { const count = repeater.count; for (let i = 0; i < count; i++) { const item = repeater.itemAt(i); if (item?.enabled) return item; } return null; } function findLastEnabled(): Item { for (let i = repeater.count - 1; i >= 0; i--) { const item = repeater.itemAt(i); if (item?.enabled) return item; } return null; } asynchronous: true Layout.alignment: Qt.AlignHCenter // Cursed ahh thing to add padding to first and last enabled components Layout.topMargin: findFirstEnabled() === this ? root.vPadding : 0 Layout.bottomMargin: findLastEnabled() === this ? root.vPadding : 0 visible: enabled active: enabled } } ================================================ FILE: modules/bar/BarWrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import qs.modules.bar.popouts as BarPopouts import Quickshell import QtQuick Item { id: root required property ShellScreen screen required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts required property bool disabled readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered) property bool isHovered function closeTray(): void { (content.item as Bar)?.closeTray(); } function checkPopout(y: real): void { (content.item as Bar)?.checkPopout(y); } function handleWheel(y: real, angleDelta: point): void { (content.item as Bar)?.handleWheel(y, angleDelta); } visible: width > Config.border.thickness implicitWidth: Config.border.thickness states: State { name: "visible" when: root.shouldBeVisible PropertyChanges { root.implicitWidth: root.contentWidth } } transitions: [ Transition { from: "" to: "visible" Anim { target: root property: "implicitWidth" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } }, Transition { from: "visible" to: "" Anim { target: root property: "implicitWidth" easing.bezierCurve: Appearance.anim.curves.emphasized } } ] Loader { id: content anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right active: root.shouldBeVisible || root.visible sourceComponent: Bar { width: root.contentWidth screen: root.screen visibilities: root.visibilities popouts: root.popouts } } } ================================================ FILE: modules/bar/components/ActiveWindow.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.utils import qs.config import QtQuick Item { id: root required property var bar required property Brightness.Monitor monitor property color colour: Colours.palette.m3primary readonly property string windowTitle: { const title = Hypr.activeToplevel?.title; if (!title) return qsTr("Desktop"); if (Config.bar.activeWindow.compact) { // " - " (standard hyphen), " — " (em dash), " – " (en dash) const parts = title.split(/\s+[\-\u2013\u2014]\s+/); if (parts.length > 1) return parts[parts.length - 1].trim(); } return title; } readonly property int maxHeight: { const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer"); const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0); // Length - 2 cause repeater counts as a child return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2; } property Title current: text1 clip: true implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight) implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin Loader { asynchronous: true anchors.fill: parent active: !Config.bar.activeWindow.showOnHover sourceComponent: MouseArea { cursorShape: Qt.PointingHandCursor hoverEnabled: true onPositionChanged: { const popouts = root.bar.popouts; if (popouts.hasCurrent && popouts.currentName !== "activewindow") popouts.hasCurrent = false; } onClicked: { const popouts = root.bar.popouts; if (popouts.hasCurrent) { popouts.hasCurrent = false; } else { popouts.currentName = "activewindow"; popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y; popouts.hasCurrent = true; } } } } MaterialIcon { id: icon anchors.horizontalCenter: parent.horizontalCenter animate: true text: Icons.getAppCategoryIcon(Hypr.activeToplevel?.lastIpcObject.class, "desktop_windows") color: root.colour } Title { id: text1 } Title { id: text2 } TextMetrics { id: metrics text: root.windowTitle font.pointSize: Appearance.font.size.smaller font.family: Appearance.font.family.mono elide: Qt.ElideRight elideWidth: root.maxHeight - icon.height onTextChanged: { const next = root.current === text1 ? text2 : text1; next.text = elidedText; root.current = next; } onElideWidthChanged: root.current.text = elidedText } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } component Title: StyledText { id: text anchors.horizontalCenter: icon.horizontalCenter anchors.top: icon.bottom anchors.topMargin: Appearance.spacing.small font.pointSize: metrics.font.pointSize font.family: metrics.font.family color: root.colour opacity: root.current === this ? 1 : 0 transform: [ Translate { x: Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0 }, Rotation { angle: Config.bar.activeWindow.inverted ? 270 : 90 origin.x: text.implicitHeight / 2 origin.y: text.implicitHeight / 2 } ] width: implicitHeight height: implicitWidth Behavior on opacity { Anim {} } } } ================================================ FILE: modules/bar/components/Clock.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import QtQuick StyledRect { id: root readonly property color colour: Colours.palette.m3tertiary readonly property int padding: Config.bar.clock.background ? Appearance.padding.normal : Appearance.padding.small implicitWidth: Config.bar.sizes.innerWidth implicitHeight: layout.implicitHeight + root.padding * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.full Column { id: layout anchors.centerIn: parent spacing: Appearance.spacing.small Loader { asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: Config.bar.clock.showIcon visible: active sourceComponent: MaterialIcon { text: "calendar_month" color: root.colour } } StyledText { anchors.horizontalCenter: parent.horizontalCenter visible: Config.bar.clock.showDate horizontalAlignment: StyledText.AlignHCenter text: Time.format("ddd\nd") font.pointSize: Appearance.font.size.smaller font.family: Appearance.font.family.sans color: root.colour } Rectangle { anchors.horizontalCenter: parent.horizontalCenter visible: Config.bar.clock.showDate height: visible ? 1 : 0 width: parent.width * 0.8 color: root.colour opacity: 0.2 } StyledText { anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: StyledText.AlignHCenter text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") font.pointSize: Appearance.font.size.smaller font.family: Appearance.font.family.mono color: root.colour } } } ================================================ FILE: modules/bar/components/OsIcon.qml ================================================ import qs.components import qs.components.effects import qs.services import qs.config import qs.utils import QtQuick Item { id: root implicitWidth: Math.round(Appearance.font.size.large * 1.2) implicitHeight: Math.round(Appearance.font.size.large * 1.2) MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { const visibilities = Visibilities.getForActive(); visibilities.launcher = !visibilities.launcher; } } Loader { asynchronous: true anchors.centerIn: parent sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } Component { id: caelestiaLogo Logo { implicitWidth: Math.round(Appearance.font.size.large * 1.6) implicitHeight: Math.round(Appearance.font.size.large * 1.6) } } Component { id: distroIcon ColouredIcon { source: SysInfo.osLogo implicitSize: Math.round(Appearance.font.size.large * 1.2) colour: Colours.palette.m3tertiary } } } ================================================ FILE: modules/bar/components/Power.qml ================================================ import qs.components import qs.services import qs.config import QtQuick Item { id: root required property DrawerVisibilities visibilities implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { // Cursed workaround to make the height larger than the parent function onClicked(): void { root.visibilities.session = !root.visibilities.session; } anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.full } MaterialIcon { id: icon anchors.centerIn: parent anchors.horizontalCenterOffset: -1 text: "power_settings_new" color: Colours.palette.m3error font.bold: true font.pointSize: Appearance.font.size.normal } } ================================================ FILE: modules/bar/components/Settings.qml ================================================ import qs.components import qs.modules.controlcenter import qs.services import qs.config import QtQuick Item { id: root implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { // Cursed workaround to make the height larger than the parent function onClicked(): void { WindowFactory.create(null, { active: "network" }); } anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.full } MaterialIcon { id: icon anchors.centerIn: parent anchors.horizontalCenterOffset: -1 text: "settings" color: Colours.palette.m3onSurface font.bold: true font.pointSize: Appearance.font.size.normal } } ================================================ FILE: modules/bar/components/SettingsIcon.qml ================================================ import qs.components import qs.modules.controlcenter import qs.services import qs.config import QtQuick Item { id: root implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { // Cursed workaround to make the height larger than the parent function onClicked(): void { WindowFactory.create(null, { active: "network" }); } anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.full } MaterialIcon { id: icon anchors.centerIn: parent anchors.horizontalCenterOffset: -1 text: "settings" color: Colours.palette.m3onSurface font.bold: true font.pointSize: Appearance.font.size.normal } } ================================================ FILE: modules/bar/components/StatusIcons.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.utils import qs.config import Quickshell import Quickshell.Bluetooth import Quickshell.Services.UPower import QtQuick import QtQuick.Layouts StyledRect { id: root property color colour: Colours.palette.m3secondary readonly property alias items: iconColumn color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.full clip: true implicitWidth: Config.bar.sizes.innerWidth implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) ColumnLayout { id: iconColumn anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.bottomMargin: Appearance.padding.normal spacing: Appearance.spacing.smaller / 2 // Lock keys status WrappedLoader { name: "lockstatus" active: Config.bar.status.showLockStatus sourceComponent: ColumnLayout { spacing: 0 Item { implicitWidth: capslockIcon.implicitWidth implicitHeight: Hypr.capsLock ? capslockIcon.implicitHeight : 0 MaterialIcon { id: capslockIcon anchors.centerIn: parent scale: Hypr.capsLock ? 1 : 0.5 opacity: Hypr.capsLock ? 1 : 0 text: "keyboard_capslock_badge" color: root.colour Behavior on opacity { Anim {} } Behavior on scale { Anim {} } } Behavior on implicitHeight { Anim {} } } Item { Layout.topMargin: Hypr.capsLock && Hypr.numLock ? iconColumn.spacing : 0 implicitWidth: numlockIcon.implicitWidth implicitHeight: Hypr.numLock ? numlockIcon.implicitHeight : 0 MaterialIcon { id: numlockIcon anchors.centerIn: parent scale: Hypr.numLock ? 1 : 0.5 opacity: Hypr.numLock ? 1 : 0 text: "looks_one" color: root.colour Behavior on opacity { Anim {} } Behavior on scale { Anim {} } } Behavior on implicitHeight { Anim {} } } } } // Audio icon WrappedLoader { name: "audio" active: Config.bar.status.showAudio sourceComponent: MaterialIcon { animate: true text: Icons.getVolumeIcon(Audio.volume, Audio.muted) color: root.colour } } // Microphone icon WrappedLoader { name: "audio" active: Config.bar.status.showMicrophone sourceComponent: MaterialIcon { animate: true text: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted) color: root.colour } } // Keyboard layout icon WrappedLoader { name: "kblayout" active: Config.bar.status.showKbLayout sourceComponent: StyledText { animate: true text: Hypr.kbLayout color: root.colour font.family: Appearance.font.family.mono } } // Network icon WrappedLoader { name: "network" active: Config.bar.status.showNetwork && (!Nmcli.activeEthernet || Config.bar.status.showWifi) sourceComponent: MaterialIcon { animate: true text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off" color: root.colour } } // Ethernet icon WrappedLoader { name: "ethernet" active: Config.bar.status.showNetwork && Nmcli.activeEthernet sourceComponent: MaterialIcon { animate: true text: "cable" color: root.colour } } // Bluetooth section WrappedLoader { Layout.preferredHeight: implicitHeight name: "bluetooth" active: Config.bar.status.showBluetooth sourceComponent: ColumnLayout { spacing: Appearance.spacing.smaller / 2 // Bluetooth icon MaterialIcon { animate: true text: { if (!Bluetooth.defaultAdapter?.enabled) return "bluetooth_disabled"; if (Bluetooth.devices.values.some(d => d.connected)) return "bluetooth_connected"; return "bluetooth"; } color: root.colour } // Connected bluetooth devices Repeater { model: ScriptModel { values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) } MaterialIcon { id: device required property BluetoothDevice modelData animate: true text: Icons.getBluetoothIcon(modelData?.icon) color: root.colour fill: 1 SequentialAnimation on opacity { running: device.modelData?.state !== BluetoothDeviceState.Connected alwaysRunToEnd: true loops: Animation.Infinite Anim { from: 1 to: 0 duration: Appearance.anim.durations.large easing.bezierCurve: Appearance.anim.curves.standardAccel } Anim { from: 0 to: 1 duration: Appearance.anim.durations.large easing.bezierCurve: Appearance.anim.curves.standardDecel } } } } } Behavior on Layout.preferredHeight { Anim {} } } // Battery icon WrappedLoader { name: "battery" active: Config.bar.status.showBattery sourceComponent: MaterialIcon { animate: true text: { if (!UPower.displayDevice.isLaptopBattery) { if (PowerProfiles.profile === PowerProfile.PowerSaver) return "energy_savings_leaf"; if (PowerProfiles.profile === PowerProfile.Performance) return "rocket_launch"; return "balance"; } const perc = UPower.displayDevice.percentage; const charging = [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); if (perc === 1) return charging ? "battery_charging_full" : "battery_full"; let level = Math.floor(perc * 7); if (charging && (level === 4 || level === 1)) level--; return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error fill: 1 } } } component WrappedLoader: Loader { required property string name asynchronous: true Layout.alignment: Qt.AlignHCenter visible: active } } ================================================ FILE: modules/bar/components/Tray.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import Quickshell.Services.SystemTray import QtQuick StyledRect { id: root readonly property alias layout: layout readonly property alias items: items readonly property alias expandIcon: expandIcon readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0 property bool expanded readonly property real nonAnimHeight: { if (!Config.bar.tray.compact) return layout.implicitHeight + padding * 2; return (expanded ? expandIcon.implicitHeight + layout.implicitHeight + spacing : expandIcon.implicitHeight) + padding * 2; } clip: true visible: height > 0 implicitWidth: Config.bar.sizes.innerWidth implicitHeight: nonAnimHeight color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.full Column { id: layout anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: root.padding spacing: Appearance.spacing.small opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0 add: Transition { Anim { properties: "scale" from: 0 to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } } move: Transition { Anim { properties: "scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { properties: "x,y" } } Repeater { id: items model: ScriptModel { values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id)) } TrayItem {} } Behavior on opacity { Anim {} } } Loader { id: expandIcon asynchronous: true anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom active: Config.bar.tray.compact && items.count > 0 sourceComponent: Item { implicitWidth: expandIconInner.implicitWidth implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2 MaterialIcon { id: expandIconInner anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small text: "expand_less" font.pointSize: Appearance.font.size.large rotation: root.expanded ? 180 : 0 Behavior on rotation { Anim {} } Behavior on anchors.bottomMargin { Anim {} } } } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } ================================================ FILE: modules/bar/components/TrayItem.qml ================================================ pragma ComponentBehavior: Bound import qs.components.effects import qs.services import qs.config import qs.utils import Quickshell.Services.SystemTray import QtQuick MouseArea { id: root required property SystemTrayItem modelData acceptedButtons: Qt.LeftButton | Qt.RightButton implicitWidth: Appearance.font.size.small * 2 implicitHeight: Appearance.font.size.small * 2 onClicked: event => { if (event.button === Qt.LeftButton) modelData.activate(); else modelData.secondaryActivate(); } ColouredIcon { id: icon anchors.fill: parent source: Icons.getTrayIcon(root.modelData.id, root.modelData.icon) colour: Colours.palette.m3secondary layer.enabled: Config.bar.tray.recolour } } ================================================ FILE: modules/bar/components/workspaces/ActiveIndicator.qml ================================================ import qs.components import qs.components.effects import qs.services import qs.config import QtQuick StyledRect { id: root required property int activeWsId required property Repeater workspaces required property Item mask readonly property int currentWsIdx: { let i = activeWsId - 1; while (i < 0) i += Config.bar.workspaces.shown; return i % Config.bar.workspaces.shown; } property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0 property real offset: Math.min(leading, trailing) property real size: { const s = Math.abs(leading - trailing) + currentSize; if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) { const ws = workspaces.itemAt(lastWs) as Workspace; return ws ? Math.min(ws.y + ws.size - offset, s) : 0; } return s; } property int cWs property int lastWs onCurrentWsIdxChanged: { lastWs = cWs; cWs = currentWsIdx; } clip: true y: offset + mask.y implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 implicitHeight: size radius: Appearance.rounding.full color: Colours.palette.m3primary Colouriser { source: root.mask sourceColor: Colours.palette.m3onSurface colorizationColor: Colours.palette.m3onPrimary x: 0 y: -parent.offset implicitWidth: root.mask.implicitWidth implicitHeight: root.mask.implicitHeight anchors.horizontalCenter: parent.horizontalCenter } Behavior on leading { enabled: Config.bar.workspaces.activeTrail EAnim {} } Behavior on trailing { enabled: Config.bar.workspaces.activeTrail EAnim { duration: Appearance.anim.durations.normal * 2 } } Behavior on currentSize { enabled: Config.bar.workspaces.activeTrail EAnim {} } Behavior on offset { enabled: !Config.bar.workspaces.activeTrail EAnim {} } Behavior on size { enabled: !Config.bar.workspaces.activeTrail EAnim {} } component EAnim: Anim { easing.bezierCurve: Appearance.anim.curves.emphasized } } ================================================ FILE: modules/bar/components/workspaces/OccupiedBg.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import QtQuick Item { id: root required property Repeater workspaces required property var occupied required property int groupOffset property list pills: [] onOccupiedChanged: { if (!occupied) return; let count = 0; const start = groupOffset; const end = start + Config.bar.workspaces.shown; for (const [ws, occ] of Object.entries(occupied)) { if (ws > start && ws <= end && occ) { const isFirstInGroup = Number(ws) === start + 1; const isLastInGroup = Number(ws) === end; if (isFirstInGroup || !occupied[ws - 1]) { if (pills[count]) pills[count].start = ws; else pills.push(pillComp.createObject(root, { start: ws })); count++; } if ((isLastInGroup || !occupied[ws + 1]) && pills[count - 1]) pills[count - 1].end = ws; } } if (pills.length > count) pills.splice(count, pills.length - count).forEach(p => p.destroy()); } Repeater { model: ScriptModel { values: root.pills.filter(p => p) } StyledRect { id: rect required property var modelData readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null function getWsIdx(ws: int): int { let i = ws - 1; while (i < 0) i += Config.bar.workspaces.shown; return i % Config.bar.workspaces.shown; } anchors.horizontalCenter: root.horizontalCenter y: (start?.y ?? 0) - 1 implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2 implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) radius: Appearance.rounding.full scale: 0 Component.onCompleted: scale = 1 Behavior on scale { Anim { easing.bezierCurve: Appearance.anim.curves.standardDecel } } Behavior on y { Anim {} } Behavior on implicitHeight { Anim {} } } } component Pill: QtObject { property int start property int end } Component { id: pillComp Pill {} } } ================================================ FILE: modules/bar/components/workspaces/SpecialWorkspaces.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.effects import qs.services import qs.utils import qs.config import Quickshell import Quickshell.Hyprland import QtQuick import QtQuick.Layouts Item { id: root required property ShellScreen screen readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" layer.enabled: true layer.effect: OpacityMask { maskSource: mask } Item { id: mask anchors.fill: parent layer.enabled: true visible: false Rectangle { anchors.fill: parent radius: Appearance.rounding.full gradient: Gradient { orientation: Gradient.Vertical GradientStop { position: 0 color: Qt.rgba(0, 0, 0, 0) } GradientStop { position: 0.3 color: Qt.rgba(0, 0, 0, 1) } GradientStop { position: 0.7 color: Qt.rgba(0, 0, 0, 1) } GradientStop { position: 1 color: Qt.rgba(0, 0, 0, 0) } } } Rectangle { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right radius: Appearance.rounding.full implicitHeight: parent.height / 2 opacity: view.contentY > 0 ? 0 : 1 Behavior on opacity { Anim {} } } Rectangle { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right radius: Appearance.rounding.full implicitHeight: parent.height / 2 opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1 Behavior on opacity { Anim {} } } } ListView { id: view anchors.fill: parent spacing: Appearance.spacing.normal interactive: false currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial)) model: ScriptModel { values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) } preferredHighlightBegin: 0 preferredHighlightEnd: height highlightRangeMode: ListView.StrictlyEnforceRange highlightFollowsCurrentItem: false highlight: Item { y: view.currentItem?.y ?? 0 implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 Behavior on y { Anim {} } } delegate: SpecialWsDelegate {} add: Transition { Anim { properties: "scale" from: 0 to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } } remove: Transition { Anim { property: "scale" to: 0.5 duration: Appearance.anim.durations.small } Anim { property: "opacity" to: 0 duration: Appearance.anim.durations.small } } move: Transition { Anim { properties: "scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { properties: "x,y" } } displaced: Transition { Anim { properties: "scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { properties: "x,y" } } } component SpecialWsDelegate: ColumnLayout { id: ws required property HyprlandWorkspace modelData readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) property int wsId property string icon property bool hasWindows anchors.left: view.contentItem.left anchors.right: view.contentItem.right spacing: 0 Component.onCompleted: { wsId = modelData.id; icon = Icons.getSpecialWsIcon(modelData.name); hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; } // Hacky thing cause modelData gets destroyed before the remove anim finishes Connections { function onIdChanged(): void { if (ws.modelData) ws.wsId = ws.modelData.id; } function onNameChanged(): void { if (ws.modelData) ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); } function onLastIpcObjectChanged(): void { if (ws.modelData) ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; } target: ws.modelData } Connections { function onShowWindowsOnSpecialWorkspacesChanged(): void { if (ws.modelData) ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; } target: Config.bar.workspaces } Loader { id: label asynchronous: true Layout.alignment: Qt.AlignHCenter | Qt.AlignTop Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 sourceComponent: ws.icon.length === 1 ? letterComp : iconComp Component { id: iconComp MaterialIcon { fill: 1 text: ws.icon verticalAlignment: Qt.AlignVCenter } } Component { id: letterComp StyledText { text: ws.icon verticalAlignment: Qt.AlignVCenter } } } Loader { id: windows asynchronous: true Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true Layout.preferredHeight: implicitHeight visible: active active: ws.hasWindows sourceComponent: Column { spacing: 0 add: Transition { Anim { properties: "scale" from: 0 to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } } move: Transition { Anim { properties: "scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { properties: "x,y" } } Repeater { model: ScriptModel { values: { const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); const maxIcons = Config.bar.workspaces.maxWindowIcons; return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; } } MaterialIcon { required property var modelData grade: 0 text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") color: Colours.palette.m3onSurfaceVariant } } } Behavior on Layout.preferredHeight { Anim {} } } } Loader { asynchronous: true active: Config.bar.workspaces.activeIndicator anchors.fill: parent sourceComponent: Item { StyledClippingRect { id: indicator anchors.left: parent.left anchors.right: parent.right y: (view.currentItem?.y ?? 0) - view.contentY implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 color: Colours.palette.m3tertiary radius: Appearance.rounding.full Colouriser { source: view sourceColor: Colours.palette.m3onSurface colorizationColor: Colours.palette.m3onTertiary anchors.horizontalCenter: parent.horizontalCenter x: 0 y: -indicator.y implicitWidth: view.width implicitHeight: view.height } Behavior on y { Anim { easing.bezierCurve: Appearance.anim.curves.emphasized } } Behavior on implicitHeight { Anim { easing.bezierCurve: Appearance.anim.curves.emphasized } } } } } MouseArea { property real startY anchors.fill: view drag.target: view.contentItem drag.axis: Drag.YAxis drag.maximumY: 0 drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) onPressed: event => startY = event.y onClicked: event => { if (Math.abs(event.y - startY) > drag.threshold) return; const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; if (ws?.modelData) Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); else Hypr.dispatch("togglespecialworkspace special"); } } } ================================================ FILE: modules/bar/components/workspaces/Workspace.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.utils import qs.config import Quickshell import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property int index required property int activeWsId required property var occupied required property int groupOffset readonly property bool isWorkspace: true // Flag for finding workspace children // Unanimated prop for others to use as reference readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0) readonly property int ws: groupOffset + index + 1 readonly property bool isOccupied: occupied[ws] ?? false readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows Layout.alignment: Qt.AlignHCenter Layout.preferredHeight: size spacing: 0 StyledText { id: indicator Layout.alignment: Qt.AlignHCenter | Qt.AlignTop Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 animate: true text: { const ws = Hypr.workspaces.values.find(w => w.id === root.ws); const wsName = !ws || ws.name == root.ws ? root.ws : ws.name[0]; let displayName = wsName.toString(); if (Config.bar.workspaces.capitalisation.toLowerCase() === "upper") { displayName = displayName.toUpperCase(); } else if (Config.bar.workspaces.capitalisation.toLowerCase() === "lower") { displayName = displayName.toLowerCase(); } const label = Config.bar.workspaces.label || displayName; const occupiedLabel = Config.bar.workspaces.occupiedLabel || label; const activeLabel = Config.bar.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label); return root.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label; } color: Config.bar.workspaces.occupiedBg || root.isOccupied || root.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.layer(Colours.palette.m3outlineVariant, 2) verticalAlignment: Qt.AlignVCenter } Loader { id: windows asynchronous: true Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true Layout.topMargin: -Config.bar.sizes.innerWidth / 10 visible: active active: root.hasWindows sourceComponent: Column { spacing: 0 add: Transition { Anim { properties: "scale" from: 0 to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } } move: Transition { Anim { properties: "scale" to: 1 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { properties: "x,y" } } Repeater { model: ScriptModel { values: { const ws = root.ws; const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); const maxIcons = Config.bar.workspaces.maxWindowIcons; return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; } } MaterialIcon { required property var modelData grade: 0 text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") color: Colours.palette.m3onSurfaceVariant } } } } Behavior on Layout.preferredHeight { Anim {} } } ================================================ FILE: modules/bar/components/workspaces/Workspaces.qml ================================================ pragma ComponentBehavior: Bound import qs.services import qs.config import qs.components import Quickshell import QtQuick import QtQuick.Layouts import QtQuick.Effects StyledClippingRect { id: root required property ShellScreen screen readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId readonly property var occupied: { const occ = {}; for (const ws of Hypr.workspaces.values) occ[ws.id] = ws.lastIpcObject.windows > 0; return occ; } readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown property real blur: onSpecial ? 1 : 0 implicitWidth: Config.bar.sizes.innerWidth implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.full Item { anchors.fill: parent scale: root.onSpecial ? 0.8 : 1 opacity: root.onSpecial ? 0.5 : 1 layer.enabled: root.blur > 0 layer.effect: MultiEffect { blurEnabled: true blur: root.blur blurMax: 32 } Loader { asynchronous: true active: Config.bar.workspaces.occupiedBg anchors.fill: parent anchors.margins: Appearance.padding.small sourceComponent: OccupiedBg { workspaces: workspaces occupied: root.occupied groupOffset: root.groupOffset } } ColumnLayout { id: layout anchors.centerIn: parent spacing: Math.floor(Appearance.spacing.small / 2) Repeater { id: workspaces model: Config.bar.workspaces.shown Workspace { activeWsId: root.activeWsId occupied: root.occupied groupOffset: root.groupOffset } } } Loader { asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: Config.bar.workspaces.activeIndicator sourceComponent: ActiveIndicator { activeWsId: root.activeWsId workspaces: workspaces mask: layout } } MouseArea { anchors.fill: layout onClicked: event => { const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws; if (Hypr.activeWsId !== ws) Hypr.dispatch(`workspace ${ws}`); else Hypr.dispatch("togglespecialworkspace special"); } } Behavior on scale { Anim {} } Behavior on opacity { Anim {} } } Loader { id: specialWs asynchronous: true anchors.fill: parent anchors.margins: Appearance.padding.small active: opacity > 0 scale: root.onSpecial ? 1 : 0.5 opacity: root.onSpecial ? 1 : 0 sourceComponent: SpecialWorkspaces { screen: root.screen } Behavior on scale { Anim {} } Behavior on opacity { Anim {} } } Behavior on blur { Anim { duration: Appearance.anim.durations.small } } } ================================================ FILE: modules/bar/popouts/ActiveWindow.qml ================================================ import qs.components import qs.services import qs.utils import qs.config import Quickshell.Widgets import Quickshell.Wayland import QtQuick import QtQuick.Layouts Item { id: root required property PopoutState popouts implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 implicitHeight: child.implicitHeight Column { id: child anchors.centerIn: parent spacing: Appearance.spacing.normal RowLayout { id: detailsRow anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.normal IconImage { id: icon asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: details.implicitHeight source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing") } ColumnLayout { id: details spacing: 0 Layout.fillWidth: true StyledText { Layout.fillWidth: true text: Hypr.activeToplevel?.title ?? "" font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } StyledText { Layout.fillWidth: true text: Hypr.activeToplevel?.lastIpcObject.class ?? "" color: Colours.palette.m3onSurfaceVariant elide: Text.ElideRight } } Item { implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2 implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2 Layout.alignment: Qt.AlignVCenter StateLayer { function onClicked(): void { root.popouts.detachRequested("winfo"); } radius: Appearance.rounding.normal } MaterialIcon { id: expandIcon anchors.centerIn: parent anchors.horizontalCenterOffset: font.pointSize * 0.05 text: "chevron_right" font.pointSize: Appearance.font.size.large } } } ClippingWrapperRectangle { color: "transparent" radius: Appearance.rounding.small ScreencopyView { id: preview captureSource: Hypr.activeToplevel?.wayland ?? null live: visible constraintSize.width: Config.bar.sizes.windowPreviewSize constraintSize.height: Config.bar.sizes.windowPreviewSize } } } } ================================================ FILE: modules/bar/popouts/Audio.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import Quickshell.Services.Pipewire import QtQuick import QtQuick.Layouts import QtQuick.Controls Item { id: root required property PopoutState popouts implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 ButtonGroup { id: sinks } ButtonGroup { id: sources } ColumnLayout { id: layout anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter spacing: Appearance.spacing.normal StyledText { text: qsTr("Output device") font.weight: 500 } Repeater { model: Audio.sinks StyledRadioButton { id: control required property PwNode modelData ButtonGroup.group: sinks checked: Audio.sink?.id === modelData.id onClicked: Audio.setAudioSink(modelData) text: modelData.description } } StyledText { Layout.topMargin: Appearance.spacing.smaller text: qsTr("Input device") font.weight: 500 } Repeater { model: Audio.sources StyledRadioButton { required property PwNode modelData ButtonGroup.group: sources checked: Audio.source?.id === modelData.id onClicked: Audio.setAudioSource(modelData) text: modelData.description } } StyledText { Layout.topMargin: Appearance.spacing.smaller Layout.bottomMargin: -Appearance.spacing.small / 2 text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) font.weight: 500 } CustomMouseArea { Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 onWheel: event => { if (event.angleDelta.y > 0) Audio.incrementVolume(); else if (event.angleDelta.y < 0) Audio.decrementVolume(); } StyledSlider { anchors.left: parent.left anchors.right: parent.right implicitHeight: parent.implicitHeight value: Audio.volume onMoved: Audio.setVolume(value) Behavior on value { Anim {} } } } IconTextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer verticalPadding: Appearance.padding.small text: qsTr("Open settings") icon: "settings" onClicked: root.popouts.detachRequested("audio") } } } ================================================ FILE: modules/bar/popouts/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper required property bool invertBottomRounding readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding property real ibr: invertBottomRounding ? -1 : 1 property real sideRounding: startX > 0 ? -1 : 1 strokeWidth: -1 fillColor: Colours.palette.m3surface PathArc { relativeX: root.roundingX relativeY: root.rounding * root.sideRounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise } PathLine { relativeX: root.wrapper.width - root.roundingX * 2 relativeY: 0 } PathArc { relativeX: root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding } PathLine { relativeX: 0 relativeY: root.wrapper.height - root.rounding * 2 } PathArc { relativeX: -root.roundingX * root.ibr relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise } PathLine { relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr) relativeY: 0 } PathArc { relativeX: -root.roundingX relativeY: root.rounding * root.sideRounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise } Behavior on fillColor { CAnim {} } Behavior on ibr { Anim {} } Behavior on sideRounding { Anim {} } } ================================================ FILE: modules/bar/popouts/Battery.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell.Services.UPower import QtQuick Column { id: root spacing: Appearance.spacing.normal width: Config.bar.sizes.batteryWidth StyledText { text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected") } StyledText { function formatSeconds(s: int, fallback: string): string { const day = Math.floor(s / 86400); const hr = Math.floor(s / 3600) % 60; const min = Math.floor(s / 60) % 60; let comps = []; if (day > 0) comps.push(`${day} days`); if (hr > 0) comps.push(`${hr} hours`); if (min > 0) comps.push(`${min} mins`); return comps.join(", ") || fallback; } 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)) } Loader { asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2 color: Colours.palette.m3error radius: Appearance.rounding.normal Column { id: child anchors.centerIn: parent Row { anchors.horizontalCenter: parent.horizontalCenter spacing: Appearance.spacing.small MaterialIcon { anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: -font.pointSize / 10 text: "warning" color: Colours.palette.m3onError } StyledText { anchors.verticalCenter: parent.verticalCenter text: qsTr("Performance Degraded") color: Colours.palette.m3onError font.family: Appearance.font.family.mono font.weight: 500 } MaterialIcon { anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: -font.pointSize / 10 text: "warning" color: Colours.palette.m3onError } } StyledText { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason)) color: Colours.palette.m3onError } } } } StyledRect { id: profiles property string current: { const p = PowerProfiles.profile; if (p === PowerProfile.PowerSaver) return saver.icon; if (p === PowerProfile.Performance) return perf.icon; return balance.icon; } anchors.horizontalCenter: parent.horizontalCenter implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2 implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.full StyledRect { id: indicator color: Colours.palette.m3primary radius: Appearance.rounding.full state: profiles.current states: [ State { name: saver.icon Fill { item: saver } }, State { name: balance.icon Fill { item: balance } }, State { name: perf.icon Fill { item: perf } } ] transitions: Transition { AnchorAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.emphasized } } } Profile { id: saver anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: Appearance.padding.small profile: PowerProfile.PowerSaver icon: "energy_savings_leaf" } Profile { id: balance anchors.centerIn: parent profile: PowerProfile.Balanced icon: "balance" } Profile { id: perf anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: Appearance.padding.small profile: PowerProfile.Performance icon: "rocket_launch" } } component Fill: AnchorChanges { required property Item item target: indicator anchors.left: item.left anchors.right: item.right anchors.top: item.top anchors.bottom: item.bottom } component Profile: Item { required property string icon required property int profile implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 StateLayer { function onClicked(): void { PowerProfiles.profile = parent.profile; } radius: Appearance.rounding.full color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { id: icon anchors.centerIn: parent text: parent.icon font.pointSize: Appearance.font.size.large color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface fill: profiles.current === text ? 1 : 0 Behavior on fill { Anim {} } } } } ================================================ FILE: modules/bar/popouts/Bluetooth.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import Quickshell import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property PopoutState popouts spacing: Appearance.spacing.small StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small text: qsTr("Bluetooth") font.weight: 500 } Toggle { label: qsTr("Enabled") checked: Bluetooth.defaultAdapter?.enabled ?? false toggle.onToggled: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = checked; } } Toggle { label: qsTr("Discovering") checked: Bluetooth.defaultAdapter?.discovering ?? false toggle.onToggled: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discovering = checked; } } StyledText { Layout.topMargin: Appearance.spacing.small Layout.rightMargin: Appearance.padding.small text: { const devices = Bluetooth.devices.values; let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); const connected = devices.filter(d => d.connected).length; if (connected > 0) available += qsTr(" (%1 connected)").arg(connected); return available; } color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } Repeater { model: ScriptModel { values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) } RowLayout { id: device required property BluetoothDevice modelData readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small spacing: Appearance.spacing.small opacity: 0 scale: 0.7 Component.onCompleted: { opacity = 1; scale = 1; } Behavior on opacity { Anim {} } Behavior on scale { Anim {} } MaterialIcon { text: Icons.getBluetoothIcon(device.modelData.icon) } StyledText { Layout.leftMargin: Appearance.spacing.small / 2 Layout.rightMargin: Appearance.spacing.small / 2 Layout.fillWidth: true text: device.modelData.name } StyledRect { id: connectBtn implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.small radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) CircularIndicator { anchors.fill: parent running: device.loading } StateLayer { function onClicked(): void { device.modelData.connected = !device.modelData.connected; } color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface disabled: device.loading } MaterialIcon { id: connectIcon anchors.centerIn: parent animate: true text: device.modelData.connected ? "link_off" : "link" color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface opacity: device.loading ? 0 : 1 Behavior on opacity { Anim {} } } } Loader { asynchronous: true active: device.modelData.bonded sourceComponent: Item { implicitWidth: connectBtn.implicitWidth implicitHeight: connectBtn.implicitHeight StateLayer { function onClicked(): void { device.modelData.forget(); } radius: Appearance.rounding.full } MaterialIcon { anchors.centerIn: parent text: "delete" } } } } } IconTextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer verticalPadding: Appearance.padding.small text: qsTr("Open settings") icon: "settings" onClicked: root.popouts.detachRequested("bluetooth") } component Toggle: RowLayout { required property string label property alias checked: toggle.checked property alias toggle: toggle Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: parent.label } StyledSwitch { id: toggle } } } ================================================ FILE: modules/bar/popouts/Content.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import Quickshell import Quickshell.Services.SystemTray import QtQuick import "./kblayout" Item { id: root required property PopoutState popouts readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null anchors.centerIn: parent implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2 implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2 Item { id: content anchors.fill: parent anchors.margins: Appearance.padding.large Popout { name: "activewindow" sourceComponent: ActiveWindow { popouts: root.popouts } } Popout { id: networkPopout name: "network" sourceComponent: Network { popouts: root.popouts view: "wireless" } } Popout { name: "ethernet" sourceComponent: Network { popouts: root.popouts view: "ethernet" } } Popout { id: passwordPopout name: "wirelesspassword" sourceComponent: WirelessPassword { id: passwordComponent popouts: root.popouts network: (networkPopout.item as Network)?.passwordNetwork ?? null } Connections { function onCurrentNameChanged() { // Update network immediately when password popout becomes active if (root.popouts.currentName === "wirelesspassword") { // Set network immediately if available if ((networkPopout.item as Network)?.passwordNetwork) { if (passwordPopout.item) { (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } } // Also try after a short delay in case networkPopout.item wasn't ready Qt.callLater(() => { if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) { (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } }, 100); } } target: root.popouts } Connections { function onItemChanged() { // When network popout loads, update password popout if it's active if (root.popouts.currentName === "wirelesspassword" && passwordPopout.item) { Qt.callLater(() => { if ((networkPopout.item as Network)?.passwordNetwork) { (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } }); } } target: networkPopout } } Popout { name: "bluetooth" sourceComponent: Bluetooth { popouts: root.popouts } } Popout { name: "battery" sourceComponent: Battery {} } Popout { name: "audio" sourceComponent: Audio { popouts: root.popouts } } Popout { name: "kblayout" sourceComponent: KbLayout {} } Popout { name: "lockstatus" sourceComponent: LockStatus {} } Repeater { model: ScriptModel { values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id)) } Popout { id: trayMenu required property SystemTrayItem modelData required property int index name: `traymenu${index}` sourceComponent: trayMenuComp Connections { function onHasCurrentChanged(): void { if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { trayMenu.sourceComponent = null; trayMenu.sourceComponent = trayMenuComp; } } target: root.popouts } Component { id: trayMenuComp TrayMenu { popouts: root.popouts trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type } } } } } component Popout: Loader { id: popout required property string name readonly property bool shouldBeActive: root.popouts.currentName === name anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right opacity: 0 scale: 0.8 active: false states: State { name: "active" when: popout.shouldBeActive PropertyChanges { popout.active: true popout.opacity: 1 popout.scale: 1 } } transitions: [ Transition { from: "active" to: "" SequentialAnimation { Anim { properties: "opacity,scale" duration: Appearance.anim.durations.small } PropertyAction { target: popout property: "active" } } }, Transition { from: "" to: "active" SequentialAnimation { PropertyAction { target: popout property: "active" } Anim { properties: "opacity,scale" } } } ] } } ================================================ FILE: modules/bar/popouts/LockStatus.qml ================================================ import qs.components import qs.services import qs.config import QtQuick.Layouts ColumnLayout { spacing: Appearance.spacing.small StyledText { text: qsTr("Capslock: %1").arg(Hypr.capsLock ? "Enabled" : "Disabled") } StyledText { text: qsTr("Numlock: %1").arg(Hypr.numLock ? "Enabled" : "Disabled") } } ================================================ FILE: modules/bar/popouts/Network.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import Quickshell import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property PopoutState popouts property string connectingToSsid: "" property string view: "wireless" // "wireless" or "ethernet" property var passwordNetwork: null property bool showPasswordDialog: false spacing: Appearance.spacing.small width: Config.bar.sizes.networkWidth // Wireless section StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.padding.normal : 0 Layout.rightMargin: Appearance.padding.small text: qsTr("Wireless") font.weight: 500 } Toggle { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 label: qsTr("Enabled") checked: Nmcli.wifiEnabled toggle.onToggled: Nmcli.enableWifi(checked) } StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.rightMargin: Appearance.padding.small text: qsTr("%1 networks available").arg(Nmcli.networks.length) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } Repeater { visible: root.view === "wireless" model: ScriptModel { values: [...Nmcli.networks].sort((a, b) => { if (a.active !== b.active) return b.active - a.active; return b.strength - a.strength; }).slice(0, 8) } RowLayout { id: networkItem required property Nmcli.AccessPoint modelData readonly property bool isConnecting: root.connectingToSsid === modelData.ssid readonly property bool loading: networkItem.isConnecting visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small spacing: Appearance.spacing.small opacity: 0 scale: 0.7 Component.onCompleted: { opacity = 1; scale = 1; } Behavior on opacity { Anim {} } Behavior on scale { Anim {} } MaterialIcon { text: Icons.getNetworkIcon(networkItem.modelData.strength) color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant } MaterialIcon { visible: networkItem.modelData.isSecure text: "lock" font.pointSize: Appearance.font.size.small } StyledText { Layout.leftMargin: Appearance.spacing.small / 2 Layout.rightMargin: Appearance.spacing.small / 2 Layout.fillWidth: true text: networkItem.modelData.ssid elide: Text.ElideRight font.weight: networkItem.modelData.active ? 500 : 400 color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurface } StyledRect { implicitWidth: implicitHeight implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) CircularIndicator { anchors.fill: parent running: networkItem.loading } StateLayer { function onClicked(): void { if (networkItem.modelData.active) { Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; NetworkConnection.handleConnect(networkItem.modelData, null, network => { // Password is required - show password dialog root.passwordNetwork = network; root.showPasswordDialog = true; root.popouts.currentName = "wirelesspassword"; }); // Clear connecting state if connection succeeds immediately (saved profile) // This is handled by the onActiveChanged connection below } } color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface disabled: networkItem.loading || !Nmcli.wifiEnabled } MaterialIcon { id: wirelessConnectIcon anchors.centerIn: parent animate: true text: networkItem.modelData.active ? "link_off" : "link" color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface opacity: networkItem.loading ? 0 : 1 Behavior on opacity { Anim {} } } } } } StyledRect { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.fillWidth: true implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.full color: Colours.palette.m3primaryContainer StateLayer { function onClicked(): void { Nmcli.rescanWifi(); } color: Colours.palette.m3onPrimaryContainer disabled: Nmcli.scanning || !Nmcli.wifiEnabled } RowLayout { id: rescanBtn anchors.centerIn: parent spacing: Appearance.spacing.small opacity: Nmcli.scanning ? 0 : 1 MaterialIcon { id: scanIcon Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) animate: true text: "wifi_find" color: Colours.palette.m3onPrimaryContainer } StyledText { Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575) text: qsTr("Rescan networks") color: Colours.palette.m3onPrimaryContainer } Behavior on opacity { Anim {} } } CircularIndicator { anchors.centerIn: parent strokeWidth: Appearance.padding.small / 2 bgColour: "transparent" implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2 running: Nmcli.scanning } } // Ethernet section StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.padding.normal : 0 Layout.rightMargin: Appearance.padding.small text: qsTr("Ethernet") font.weight: 500 } StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.rightMargin: Appearance.padding.small text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } Repeater { visible: root.view === "ethernet" model: ScriptModel { values: [...Nmcli.ethernetDevices].sort((a, b) => { if (a.connected !== b.connected) return b.connected - a.connected; return (a.interface || "").localeCompare(b.interface || ""); }).slice(0, 8) } RowLayout { id: ethernetItem required property var modelData readonly property bool loading: false visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small spacing: Appearance.spacing.small opacity: 0 scale: 0.7 Component.onCompleted: { opacity = 1; scale = 1; } Behavior on opacity { Anim {} } Behavior on scale { Anim {} } MaterialIcon { text: "cable" color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant } StyledText { Layout.leftMargin: Appearance.spacing.small / 2 Layout.rightMargin: Appearance.spacing.small / 2 Layout.fillWidth: true text: ethernetItem.modelData.interface || qsTr("Unknown") elide: Text.ElideRight font.weight: ethernetItem.modelData.connected ? 500 : 400 color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface } StyledRect { implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.small radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) CircularIndicator { anchors.fill: parent running: ethernetItem.loading } StateLayer { function onClicked(): void { if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); } else { Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); } } color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface disabled: ethernetItem.loading } MaterialIcon { id: connectIcon anchors.centerIn: parent animate: true text: ethernetItem.modelData.connected ? "link_off" : "link" color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface opacity: ethernetItem.loading ? 0 : 1 Behavior on opacity { Anim {} } } } } } Connections { function onActiveChanged(): void { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; // Close password dialog if we successfully connected if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { root.showPasswordDialog = false; root.passwordNetwork = null; if (root.popouts.currentName === "wirelesspassword") { root.popouts.currentName = "network"; } } } } function onScanningChanged(): void { if (!Nmcli.scanning) scanIcon.rotation = 0; } target: Nmcli } Connections { function onCurrentNameChanged(): void { // Clear password network when leaving password dialog if (root.popouts.currentName !== "wirelesspassword" && root.showPasswordDialog) { root.showPasswordDialog = false; root.passwordNetwork = null; } } target: root.popouts } component Toggle: RowLayout { required property string label property alias checked: toggle.checked property alias toggle: toggle Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: parent.label } StyledSwitch { id: toggle } } } ================================================ FILE: modules/bar/popouts/PopoutState.qml ================================================ import QtQuick QtObject { property string currentName property bool hasCurrent signal detachRequested(mode: string) } ================================================ FILE: modules/bar/popouts/TrayMenu.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Controls StackView { id: root required property PopoutState popouts required property QsMenuHandle trayItem implicitWidth: currentItem?.implicitWidth ?? 0 implicitHeight: currentItem?.implicitHeight ?? 0 initialItem: SubMenu { handle: root.trayItem } pushEnter: NoAnim {} pushExit: NoAnim {} popEnter: NoAnim {} popExit: NoAnim {} component NoAnim: Transition { NumberAnimation { duration: 0 } } component SubMenu: Column { id: menu required property QsMenuHandle handle property bool isSubMenu property bool shown padding: Appearance.padding.smaller spacing: Appearance.spacing.small opacity: shown ? 1 : 0 scale: shown ? 1 : 0.8 Component.onCompleted: shown = true StackView.onActivating: shown = true StackView.onDeactivating: shown = false StackView.onRemoved: destroy() Behavior on opacity { Anim {} } Behavior on scale { Anim {} } QsMenuOpener { id: menuOpener menu: menu.handle } Repeater { model: menuOpener.children StyledRect { id: item required property QsMenuEntry modelData implicitWidth: Config.bar.sizes.trayMenuWidth implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight radius: Appearance.rounding.full color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent" Loader { id: children asynchronous: true anchors.left: parent.left anchors.right: parent.right active: !item.modelData.isSeparator sourceComponent: Item { implicitHeight: label.implicitHeight StateLayer { function onClicked(): void { const entry = item.modelData; if (entry.hasChildren) root.push(subMenuComp.createObject(null, { handle: entry, isSubMenu: true })); else { item.modelData.triggered(); root.popouts.hasCurrent = false; } } anchors.margins: -Appearance.padding.small / 2 anchors.leftMargin: -Appearance.padding.smaller anchors.rightMargin: -Appearance.padding.smaller radius: item.radius disabled: !item.modelData.enabled } Loader { id: icon asynchronous: true anchors.left: parent.left active: item.modelData.icon !== "" sourceComponent: IconImage { asynchronous: true implicitSize: label.implicitHeight source: item.modelData.icon } } StyledText { id: label anchors.left: icon.right anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0 text: labelMetrics.elidedText color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline } TextMetrics { id: labelMetrics text: item.modelData.text font.pointSize: label.font.pointSize font.family: label.font.family elide: Text.ElideRight elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0) } Loader { id: expand asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right active: item.modelData.hasChildren sourceComponent: MaterialIcon { text: "chevron_right" color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline } } } } } } Loader { asynchronous: true active: menu.isSubMenu sourceComponent: Item { implicitWidth: back.implicitWidth implicitHeight: back.implicitHeight + Appearance.spacing.small / 2 Item { anchors.bottom: parent.bottom implicitWidth: back.implicitWidth implicitHeight: back.implicitHeight StyledRect { anchors.fill: parent anchors.margins: -Appearance.padding.small / 2 anchors.leftMargin: -Appearance.padding.smaller anchors.rightMargin: -Appearance.padding.smaller * 2 radius: Appearance.rounding.full color: Colours.palette.m3secondaryContainer StateLayer { function onClicked(): void { root.pop(); } radius: parent.radius color: Colours.palette.m3onSecondaryContainer } } Row { id: back anchors.verticalCenter: parent.verticalCenter MaterialIcon { anchors.verticalCenter: parent.verticalCenter text: "chevron_left" color: Colours.palette.m3onSecondaryContainer } StyledText { anchors.verticalCenter: parent.verticalCenter text: qsTr("Back") color: Colours.palette.m3onSecondaryContainer } } } } } } Component { id: subMenuComp SubMenu {} } } ================================================ FILE: modules/bar/popouts/WirelessPassword.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import Quickshell import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property PopoutState popouts property var network: null property bool isClosing: false readonly property bool shouldBeVisible: root.popouts.currentName === "wirelesspassword" function checkConnectionStatus(): void { if (!root.shouldBeVisible || !connectButton.connecting) { return; } // Check if we're connected to the target network (case-insensitive SSID comparison) const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (isConnected) { // Successfully connected - give it a moment for network list to update // Use Timer for actual delay connectionSuccessTimer.start(); return; } // Check for connection failures - if pending connection was cleared but we're not connected if (Nmcli.pendingConnection === null && connectButton.connecting) { // Wait a bit more before giving up (allow time for connection to establish) if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; // Delete the failed connection if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } } } function closeDialog(): void { if (isClosing) { return; } isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; connectButton.hasError = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); // Return to network popout if (root.popouts.currentName === "wirelesspassword") { root.popouts.currentName = "network"; } } spacing: Appearance.spacing.normal implicitWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 visible: shouldBeVisible || isClosing enabled: shouldBeVisible && !isClosing focus: enabled Component.onCompleted: { if (shouldBeVisible) { // Use Timer for actual delay to ensure dialog is fully rendered focusTimer.start(); } } onShouldBeVisibleChanged: { if (shouldBeVisible) { // Use Timer for actual delay to ensure dialog is fully rendered focusTimer.start(); } } Keys.onEscapePressed: closeDialog() Connections { function onCurrentNameChanged() { if (root.popouts.currentName === "wirelesspassword") { // Update network when popout becomes active Qt.callLater(() => { // Try to get network from parent Content's networkPopout const content = root.parent?.parent?.parent; if (content) { const networkPopout = content.children.find(c => c.name === "network"); if (networkPopout && networkPopout.item) { root.network = networkPopout.item.passwordNetwork; } } // Force focus to password container when popout becomes active // Use Timer for actual delay to ensure dialog is fully rendered focusTimer.start(); }); } } target: root.popouts } Timer { id: focusTimer interval: 150 onTriggered: { root.forceActiveFocus(); passwordContainer.forceActiveFocus(); } } StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 Keys.onEscapePressed: root.closeDialog() Behavior on opacity { Anim {} } Behavior on scale { Anim {} } ParallelAnimation { running: root.isClosing onFinished: { if (root.isClosing) { root.isClosing = false; } } Anim { target: parent property: "opacity" to: 0 } Anim { target: parent property: "scale" to: 0.7 } } ColumnLayout { id: content anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" font.pointSize: Appearance.font.size.extraLarge * 2 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") font.pointSize: Appearance.font.size.large font.weight: 500 } StyledText { id: networkNameText Layout.alignment: Qt.AlignHCenter text: { if (root.network) { const ssid = root.network.ssid; if (ssid && ssid.length > 0) { return qsTr("Network: %1").arg(ssid); } } return qsTr("Network: Unknown"); } color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } Timer { property int attempts: 0 interval: 50 running: root.shouldBeVisible && (!root.network || !root.network.ssid) repeat: true onTriggered: { attempts++; // Keep trying to get network from Network component const content = root.parent?.parent?.parent; if (content) { const networkPopout = content.children.find(c => c.name === "network"); if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) { root.network = networkPopout.item.passwordNetwork; } } // Stop if we got it or after 20 attempts (1 second) if ((root.network && root.network.ssid) || attempts >= 20) { stop(); attempts = 0; } } onRunningChanged: { if (!running) { attempts = 0; } } } StyledText { id: statusText Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small visible: connectButton.connecting || connectButton.hasError text: { if (connectButton.hasError) { return qsTr("Connection failed. Please check your password and try again."); } if (connectButton.connecting) { return qsTr("Connecting..."); } return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small font.weight: 400 wrapMode: Text.WordWrap Layout.maximumWidth: parent.width - Appearance.padding.large * 2 } FocusScope { id: passwordContainer property string passwordBuffer: "" objectName: "passwordContainer" Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) focus: true activeFocusOnTab: true Keys.onPressed: event => { // Ensure we have focus when receiving keyboard input if (!activeFocus) { forceActiveFocus(); } // Clear error when user starts typing if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); } event.accepted = true; } else if (event.key === Qt.Key_Backspace) { if (event.modifiers & Qt.ControlModifier) { passwordBuffer = ""; } else { passwordBuffer = passwordBuffer.slice(0, -1); } event.accepted = true; } else if (event.text && event.text.length > 0) { passwordBuffer += event.text; event.accepted = true; } } Connections { function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { // Use Timer for actual delay to ensure focus works correctly passwordFocusTimer.start(); passwordContainer.passwordBuffer = ""; connectButton.hasError = false; } } target: root } Timer { id: passwordFocusTimer interval: 50 onTriggered: { passwordContainer.forceActiveFocus(); } } Component.onCompleted: { if (root.shouldBeVisible) { // Use Timer for actual delay to ensure focus works correctly passwordFocusTimer.start(); } } StyledRect { anchors.fill: parent radius: Appearance.rounding.normal color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) border.color: { if (connectButton.hasError) { return Colours.palette.m3error; } if (passwordContainer.activeFocus) { return Colours.palette.m3primary; } return root.shouldBeVisible ? Colours.palette.m3outline : "transparent"; } Behavior on border.color { CAnim {} } Behavior on border.width { CAnim {} } Behavior on color { CAnim {} } } StateLayer { function onClicked(): void { passwordContainer.forceActiveFocus(); } hoverEnabled: false cursorShape: Qt.IBeamCursor radius: Appearance.rounding.normal } StyledText { id: placeholder anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal font.family: Appearance.font.family.mono opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { Anim {} } } ListView { id: charList readonly property int fullWidth: count * (implicitHeight + spacing) - spacing anchors.centerIn: parent implicitWidth: fullWidth implicitHeight: Appearance.font.size.normal orientation: Qt.Horizontal spacing: Appearance.spacing.small / 2 interactive: false model: ScriptModel { values: passwordContainer.passwordBuffer.split("") } delegate: StyledRect { id: ch implicitWidth: implicitHeight implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface radius: Appearance.rounding.small / 2 opacity: 0 scale: 0 Component.onCompleted: { opacity = 1; scale = 1; } ListView.onRemove: removeAnim.start() SequentialAnimation { id: removeAnim PropertyAction { target: ch property: "ListView.delayRemove" value: true } ParallelAnimation { Anim { target: ch property: "opacity" to: 0 } Anim { target: ch property: "scale" to: 0.5 } } PropertyAction { target: ch property: "ListView.delayRemove" value: false } } Behavior on opacity { Anim {} } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } Behavior on implicitWidth { Anim {} } } } RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true spacing: Appearance.spacing.normal TextButton { id: cancelButton Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") onClicked: root.closeDialog() } TextButton { id: connectButton property bool connecting: false property bool hasError: false Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") enabled: passwordContainer.passwordBuffer.length > 0 && !connecting onClicked: { if (!root.network || connecting) { return; } const password = passwordContainer.passwordBuffer; if (!password || password.length === 0) { return; } // Clear any previous error hasError = false; // Set connecting state connecting = true; enabled = false; text = qsTr("Connecting..."); // Connect to network NetworkConnection.connectWithPassword(root.network, password, result => { if (result && result.success) // Connection successful, monitor will handle the rest {} else if (result && result.needsPassword) { // Shouldn't happen since we provided password connectionMonitor.stop(); connecting = false; hasError = true; enabled = true; text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; // Delete the failed connection if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } else { // Connection failed immediately - show error connectionMonitor.stop(); connecting = false; hasError = true; enabled = true; text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; // Delete the failed connection if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } }); // Start monitoring connection connectionMonitor.start(); } } } } } Timer { id: connectionMonitor property int repeatCount: 0 interval: 1000 repeat: true triggeredOnStart: false onTriggered: { repeatCount++; root.checkConnectionStatus(); } onRunningChanged: { if (!running) { repeatCount = 0; } } } Timer { id: connectionSuccessTimer interval: 500 onTriggered: { // Double-check connection is still active if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (stillConnected) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.text = qsTr("Connect"); // Return to network popout on successful connection if (root.popouts.currentName === "wirelesspassword") { root.popouts.currentName = "network"; } closeDialog(); } } } } Connections { function onActiveChanged() { if (root.shouldBeVisible) { root.checkConnectionStatus(); } } function onConnectionFailed(ssid: string) { if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; // Delete the failed connection Nmcli.forgetNetwork(ssid); } } target: Nmcli } } ================================================ FILE: modules/bar/popouts/Wrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import qs.modules.windowinfo import qs.modules.controlcenter import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick Item { id: root required property ShellScreen screen readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property Item current: (content.item as Content)?.current ?? null property alias currentName: popoutState.currentName property real currentCenter property alias hasCurrent: popoutState.hasCurrent readonly property PopoutState state: popoutState property string detachedMode property string queuedMode readonly property bool isDetached: detachedMode.length > 0 property int animLength: Appearance.anim.durations.normal property list animCurve: Appearance.anim.curves.emphasized function detach(mode: string): void { animLength = Appearance.anim.durations.large; if (mode === "winfo") { detachedMode = mode; } else { queuedMode = mode; detachedMode = "any"; } focus = true; } function close(): void { hasCurrent = false; animCurve = Appearance.anim.curves.emphasizedAccel; animLength = Appearance.anim.durations.normal; detachedMode = ""; animCurve = Appearance.anim.curves.emphasized; } visible: width > 0 && height > 0 clip: true implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight focus: hasCurrent Keys.onEscapePressed: { // Forward escape to password popout if active, otherwise close if (currentName === "wirelesspassword" && content.item) { const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); if (passwordPopout && passwordPopout.item) { passwordPopout.item.closeDialog(); return; } } close(); } Keys.onPressed: event => { // Don't intercept keys when password popout is active - let it handle them if (currentName === "wirelesspassword") { event.accepted = false; } } PopoutState { id: popoutState onDetachRequested: mode => root.detach(mode) } HyprlandFocusGrab { active: root.isDetached windows: [QsWindow.window] onCleared: root.close() } Binding { when: root.isDetached target: QsWindow.window property: "WlrLayershell.keyboardFocus" value: WlrKeyboardFocus.OnDemand } Binding { when: root.hasCurrent && root.currentName === "wirelesspassword" target: QsWindow.window property: "WlrLayershell.keyboardFocus" value: WlrKeyboardFocus.OnDemand } Comp { id: content shouldBeActive: root.hasCurrent && !root.detachedMode anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter sourceComponent: Content { popouts: popoutState } } Comp { shouldBeActive: root.detachedMode === "winfo" anchors.centerIn: parent sourceComponent: WindowInfo { screen: root.screen client: Hypr.activeToplevel } } Comp { shouldBeActive: root.detachedMode === "any" anchors.centerIn: parent sourceComponent: ControlCenter { function close(): void { root.close(); } screen: root.screen active: root.queuedMode } } Behavior on x { Anim { duration: root.animLength easing.bezierCurve: root.animCurve } } Behavior on y { enabled: root.implicitWidth > 0 Anim { duration: root.animLength easing.bezierCurve: root.animCurve } } Behavior on implicitWidth { Anim { duration: root.animLength easing.bezierCurve: root.animCurve } } Behavior on implicitHeight { enabled: root.implicitWidth > 0 Anim { duration: root.animLength easing.bezierCurve: root.animCurve } } component Comp: Loader { id: comp property bool shouldBeActive active: false opacity: 0 states: State { name: "active" when: comp.shouldBeActive PropertyChanges { comp.opacity: 1 comp.active: true } } transitions: [ Transition { from: "" to: "active" SequentialAnimation { PropertyAction { property: "active" } Anim { property: "opacity" } } }, Transition { from: "active" to: "" SequentialAnimation { Anim { property: "opacity" } PropertyAction { property: "active" } } } ] } } ================================================ FILE: modules/bar/popouts/kblayout/KbLayout.qml ================================================ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.components import qs.services import qs.config ColumnLayout { id: root function refresh() { kb.refresh(); } spacing: Appearance.spacing.small width: Config.bar.sizes.kbLayoutWidth Component.onCompleted: kb.start() KbLayoutModel { id: kb } StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small text: qsTr("Keyboard Layouts") font.weight: 500 } ListView { id: list model: kb.visibleModel Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small Layout.topMargin: Appearance.spacing.small clip: true interactive: true implicitHeight: Math.min(contentHeight, 320) visible: kb.visibleModel.count > 0 spacing: Appearance.spacing.small add: Transition { NumberAnimation { properties: "opacity" from: 0 to: 1 duration: 140 } NumberAnimation { properties: "y" duration: 180 easing.type: Easing.OutCubic } } remove: Transition { NumberAnimation { properties: "opacity" to: 0 duration: 100 } } move: Transition { NumberAnimation { properties: "y" duration: 180 easing.type: Easing.OutCubic } } displaced: Transition { NumberAnimation { properties: "y" duration: 180 easing.type: Easing.OutCubic } } delegate: Item { id: kbDelegate required property int layoutIndex required property string label readonly property bool isDisabled: layoutIndex > 3 width: list.width height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) ToolTip.visible: isDisabled && layer.containsMouse ToolTip.text: "XKB limitation: maximum 4 layouts allowed" StateLayer { id: layer function onClicked(): void { if (!kbDelegate.isDisabled) kb.switchTo(kbDelegate.layoutIndex); } anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height - 4 radius: Appearance.rounding.full enabled: !kbDelegate.isDisabled } StyledText { id: rowText anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right anchors.leftMargin: Appearance.padding.small anchors.rightMargin: Appearance.padding.small text: kbDelegate.label elide: Text.ElideRight opacity: kbDelegate.isDisabled ? 0.4 : 1.0 } } } Rectangle { visible: kb.activeLabel.length > 0 Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small Layout.topMargin: Appearance.spacing.small height: 1 color: Colours.palette.m3onSurfaceVariant opacity: 0.35 } RowLayout { id: activeRow visible: kb.activeLabel.length > 0 Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small Layout.topMargin: Appearance.spacing.small spacing: Appearance.spacing.small opacity: 1 scale: 1 MaterialIcon { text: "keyboard" color: Colours.palette.m3primary } StyledText { Layout.fillWidth: true text: kb.activeLabel elide: Text.ElideRight font.weight: 500 color: Colours.palette.m3primary } Connections { function onActiveLabelChanged() { if (!activeRow.visible) return; popIn.restart(); } target: kb } SequentialAnimation { id: popIn running: false ParallelAnimation { NumberAnimation { target: activeRow property: "opacity" to: 0.0 duration: 70 } NumberAnimation { target: activeRow property: "scale" to: 0.92 duration: 70 } } ParallelAnimation { NumberAnimation { target: activeRow property: "opacity" to: 1.0 duration: 160 easing.type: Easing.OutCubic } NumberAnimation { target: activeRow property: "scale" to: 1.0 duration: 220 easing.type: Easing.OutBack } } } } } ================================================ FILE: modules/bar/popouts/kblayout/KbLayoutModel.qml ================================================ pragma ComponentBehavior: Bound import QtQuick import Quickshell.Io import qs.config import Caelestia Item { id: model visible: false ListModel { id: _visibleModel } property alias visibleModel: _visibleModel property string activeLabel: "" property int activeIndex: -1 function start() { _xkbXmlBase.running = true; _getKbLayoutOpt.running = true; } function refresh() { _notifiedLimit = false; _getKbLayoutOpt.running = true; } function switchTo(idx) { _switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; _switchProc.running = true; } function _buildXmlMap(xml) { const map = {}; const re = /\s*([^<]+?)\s*<\/name>[\s\S]*?\s*([^<]+?)\s*<\/description>/g; let m; while ((m = re.exec(xml)) !== null) { const code = (m[1] || "").trim(); const desc = (m[2] || "").trim(); if (!code || !desc) continue; map[code] = _short(desc); } if (Object.keys(map).length === 0) return; _xkbMap = map; if (_layoutsModel.count > 0) { const tmp = []; for (let i = 0; i < _layoutsModel.count; i++) { const it = _layoutsModel.get(i); tmp.push({ layoutIndex: it.layoutIndex, token: it.token, label: _pretty(it.token) }); } _layoutsModel.clear(); tmp.forEach(t => _layoutsModel.append(t)); _fetchActiveLayouts.running = true; } } function _short(desc) { const m = desc.match(/^(.*)\((.*)\)$/); if (!m) return desc; const lang = m[1].trim(); const region = m[2].trim(); const code = (region.split(/[,\s-]/)[0] || region).slice(0, 2).toUpperCase(); return `${lang} (${code})`; } function _setLayouts(raw) { const parts = raw.split(",").map(s => s.trim()).filter(Boolean); _layoutsModel.clear(); const seen = new Set(); let idx = 0; for (const p of parts) { if (seen.has(p)) continue; seen.add(p); _layoutsModel.append({ layoutIndex: idx, token: p, label: _pretty(p) }); idx++; } } function _rebuildVisible() { _visibleModel.clear(); let arr = []; for (let i = 0; i < _layoutsModel.count; i++) arr.push(_layoutsModel.get(i)); arr = arr.filter(i => i.layoutIndex !== activeIndex); arr.forEach(i => _visibleModel.append(i)); if (!Config.utilities.toasts.kbLimit) return; if (_layoutsModel.count > 4) { Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); } } function _pretty(token) { const code = token.replace(/\(.*\)$/, "").trim(); if (_xkbMap[code]) return code.toUpperCase() + " - " + _xkbMap[code]; return code.toUpperCase() + " - " + code; } ListModel { id: _layoutsModel } property var _xkbMap: ({}) property bool _notifiedLimit: false Process { id: _xkbXmlBase command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] stdout: StdioCollector { onStreamFinished: model._buildXmlMap(text) } onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) _xkbXmlEvdev.running = true } Process { id: _xkbXmlEvdev command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] stdout: StdioCollector { onStreamFinished: model._buildXmlMap(text) } } Process { id: _getKbLayoutOpt command: ["hyprctl", "-j", "getoption", "input:kb_layout"] stdout: StdioCollector { onStreamFinished: { try { const j = JSON.parse(text); const raw = (j?.str || j?.value || "").toString().trim(); if (raw.length) { model._setLayouts(raw); _fetchActiveLayouts.running = true; return; } } catch (e) {} _fetchLayoutsFromDevices.running = true; } } } Process { id: _fetchLayoutsFromDevices command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { try { const dev = JSON.parse(text); const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const raw = (kb?.layout || "").trim(); if (raw.length) model._setLayouts(raw); } catch (e) {} _fetchActiveLayouts.running = true; } } } Process { id: _fetchActiveLayouts command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { try { const dev = JSON.parse(text); const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const idx = kb?.active_layout_index ?? -1; model.activeIndex = idx >= 0 ? idx : -1; model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; } catch (e) { model.activeIndex = -1; model.activeLabel = ""; } model._rebuildVisible(); } } } Process { id: _switchProc onRunningChanged: if (!running) _fetchActiveLayouts.running = true } } ================================================ FILE: modules/controlcenter/ControlCenter.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Layouts Item { id: root required property ShellScreen screen readonly property int rounding: floating ? 0 : Appearance.rounding.normal property alias floating: session.floating property alias active: session.active property alias navExpanded: session.navExpanded readonly property bool initialOpeningComplete: panes.initialOpeningComplete readonly property Session session: Session { id: session root: root } function close(): void { } implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio implicitHeight: screen.height * Config.controlCenter.sizes.heightMult GridLayout { anchors.fill: parent rowSpacing: 0 columnSpacing: 0 rows: root.floating ? 2 : 1 columns: 2 Loader { Layout.fillWidth: true Layout.columnSpan: 2 asynchronous: true active: root.floating visible: active sourceComponent: WindowTitle { screen: root.screen session: root.session } } StyledRect { Layout.fillHeight: true topLeftRadius: root.rounding bottomLeftRadius: root.rounding implicitWidth: navRail.implicitWidth color: Colours.tPalette.m3surfaceContainer CustomMouseArea { function onWheel(event: WheelEvent): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!panes.initialOpeningComplete) { return; } if (event.angleDelta.y < 0) root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1); else if (event.angleDelta.y > 0) root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); } anchors.fill: parent } NavRail { id: navRail screen: root.screen session: root.session initialOpeningComplete: root.initialOpeningComplete } } Panes { id: panes Layout.fillWidth: true Layout.fillHeight: true topRightRadius: root.rounding bottomRightRadius: root.rounding session: root.session } } } ================================================ FILE: modules/controlcenter/NavRail.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import qs.modules.controlcenter import Quickshell import QtQuick import QtQuick.Layouts Item { id: root required property ShellScreen screen required property Session session required property bool initialOpeningComplete implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 ColumnLayout { id: layout anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.padding.larger * 2 spacing: Appearance.spacing.normal states: State { name: "expanded" when: root.session.navExpanded PropertyChanges { layout.spacing: Appearance.spacing.small } } transitions: Transition { Anim { properties: "spacing" } } Loader { Layout.topMargin: Appearance.spacing.large asynchronous: true active: !root.session.floating visible: active sourceComponent: StyledRect { readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 implicitWidth: nonAnimWidth implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth color: Colours.palette.m3primaryContainer radius: Appearance.rounding.small StateLayer { id: normalWinState function onClicked(): void { root.session.root.close(); WindowFactory.create(null, { active: root.session.active, navExpanded: root.session.navExpanded }); } color: Colours.palette.m3onPrimaryContainer } MaterialIcon { id: normalWinIcon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.padding.large text: "select_window" color: Colours.palette.m3onPrimaryContainer font.pointSize: Appearance.font.size.large fill: 1 } StyledText { id: normalWinLabel anchors.left: normalWinIcon.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.spacing.normal text: qsTr("Float window") color: Colours.palette.m3onPrimaryContainer opacity: root.session.navExpanded ? 1 : 0 Behavior on opacity { Anim { duration: Appearance.anim.durations.small } } } Behavior on implicitWidth { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } Repeater { model: PaneRegistry.count NavItem { required property int index Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 icon: PaneRegistry.getByIndex(index).icon label: PaneRegistry.getByIndex(index).label } } } component NavItem: Item { id: item required property string icon required property string label readonly property bool active: root.session.active === label implicitWidth: background.implicitWidth implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin states: State { name: "expanded" when: root.session.navExpanded PropertyChanges { expandedLabel.opacity: 1 smallLabel.opacity: 0 background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 item.implicitHeight: background.implicitHeight } } transitions: Transition { Anim { property: "opacity" duration: Appearance.anim.durations.small } Anim { properties: "implicitWidth,implicitHeight" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } StyledRect { id: background radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 implicitHeight: icon.implicitHeight + Appearance.padding.small StateLayer { function onClicked(): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!root.initialOpeningComplete) { return; } root.session.active = item.label; } color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { id: icon anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.padding.large text: item.icon color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.large fill: item.active ? 1 : 0 Behavior on fill { Anim {} } } StyledText { id: expandedLabel anchors.left: icon.right anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.spacing.normal opacity: 0 text: item.label color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.capitalization: Font.Capitalize } StyledText { id: smallLabel anchors.horizontalCenter: icon.horizontalCenter anchors.top: icon.bottom anchors.topMargin: Appearance.spacing.small / 2 text: item.label font.pointSize: Appearance.font.size.small font.capitalization: Font.Capitalize } } } } ================================================ FILE: modules/controlcenter/PaneRegistry.qml ================================================ pragma Singleton import QtQuick QtObject { id: root readonly property list panes: [ QtObject { readonly property string id: "network" readonly property string label: "network" readonly property string icon: "router" readonly property string component: "network/NetworkingPane.qml" }, QtObject { readonly property string id: "bluetooth" readonly property string label: "bluetooth" readonly property string icon: "settings_bluetooth" readonly property string component: "bluetooth/BtPane.qml" }, QtObject { readonly property string id: "audio" readonly property string label: "audio" readonly property string icon: "volume_up" readonly property string component: "audio/AudioPane.qml" }, QtObject { readonly property string id: "appearance" readonly property string label: "appearance" readonly property string icon: "palette" readonly property string component: "appearance/AppearancePane.qml" }, QtObject { readonly property string id: "taskbar" readonly property string label: "taskbar" readonly property string icon: "task_alt" readonly property string component: "taskbar/TaskbarPane.qml" }, QtObject { readonly property string id: "launcher" readonly property string label: "launcher" readonly property string icon: "apps" readonly property string component: "launcher/LauncherPane.qml" }, QtObject { readonly property string id: "dashboard" readonly property string label: "dashboard" readonly property string icon: "dashboard" readonly property string component: "dashboard/DashboardPane.qml" } ] readonly property int count: panes.length readonly property var labels: { const result = []; for (let i = 0; i < panes.length; i++) { result.push(panes[i].label); } return result; } function getByIndex(index: int): QtObject { if (index >= 0 && index < panes.length) { return panes[index]; } return null; } function getIndexByLabel(label: string): int { for (let i = 0; i < panes.length; i++) { if (panes[i].label === label) { return i; } } return -1; } function getByLabel(label: string): QtObject { const index = getIndexByLabel(label); return getByIndex(index); } function getById(id: string): QtObject { for (let i = 0; i < panes.length; i++) { if (panes[i].id === id) { return panes[i]; } } return null; } } ================================================ FILE: modules/controlcenter/Panes.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import qs.modules.controlcenter import Quickshell.Widgets import QtQuick import QtQuick.Layouts ClippingRectangle { id: root required property Session session readonly property bool initialOpeningComplete: layout.initialOpeningComplete color: "transparent" clip: true focus: false activeFocusOnTab: false MouseArea { anchors.fill: parent z: -1 onPressed: function (mouse) { root.focus = true; mouse.accepted = false; } } Connections { function onActiveIndexChanged(): void { root.focus = true; } target: root.session } ColumnLayout { id: layout property bool animationComplete: true property bool initialOpeningComplete: false spacing: 0 y: -root.session.activeIndex * root.height clip: true Timer { id: animationDelayTimer interval: Appearance.anim.durations.normal onTriggered: { layout.animationComplete = true; } } Timer { id: initialOpeningTimer interval: Appearance.anim.durations.large running: true onTriggered: { layout.initialOpeningComplete = true; } } Repeater { model: PaneRegistry.count Pane { required property int index paneIndex: index componentPath: PaneRegistry.getByIndex(index).component } } Behavior on y { Anim {} } Connections { function onActiveIndexChanged(): void { layout.animationComplete = false; animationDelayTimer.restart(); } target: root.session } } component Pane: Item { id: pane required property int paneIndex required property string componentPath property bool hasBeenLoaded: false function updateActive(): void { const diff = Math.abs(root.session.activeIndex - pane.paneIndex); const isActivePane = diff === 0; let shouldBeActive = false; if (!layout.initialOpeningComplete) { shouldBeActive = isActivePane; } else { if (diff <= 1) { shouldBeActive = true; } else if (pane.hasBeenLoaded) { shouldBeActive = true; } else { shouldBeActive = layout.animationComplete; } } loader.active = shouldBeActive; } implicitWidth: root.width implicitHeight: root.height Loader { id: loader anchors.fill: parent asynchronous: true clip: false active: false Component.onCompleted: { Qt.callLater(pane.updateActive); } onActiveChanged: { if (active && !pane.hasBeenLoaded) { pane.hasBeenLoaded = true; } if (active && !item) { loader.setSource(pane.componentPath, { "session": root.session }); } } onItemChanged: { if (item) { pane.hasBeenLoaded = true; } } } Connections { function onActiveIndexChanged(): void { pane.updateActive(); } target: root.session } Connections { function onInitialOpeningCompleteChanged(): void { pane.updateActive(); } function onAnimationCompleteChanged(): void { pane.updateActive(); } target: layout } } } ================================================ FILE: modules/controlcenter/Session.qml ================================================ import QtQuick import "./state" import qs.modules.controlcenter QtObject { readonly property list panes: PaneRegistry.labels required property var root property bool floating: false property string active: "network" property int activeIndex: 0 property bool navExpanded: false readonly property BluetoothState bt: BluetoothState {} readonly property NetworkState network: NetworkState {} readonly property EthernetState ethernet: EthernetState {} readonly property LauncherState launcher: LauncherState {} readonly property VpnState vpn: VpnState {} onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active)) onActiveIndexChanged: if (panes[activeIndex]) active = panes[activeIndex] } ================================================ FILE: modules/controlcenter/WindowFactory.qml ================================================ pragma Singleton import qs.components import qs.services import Quickshell import QtQuick Singleton { id: root function create(parent: Item, props: var): void { controlCenter.createObject(parent ?? dummy, props); } QtObject { id: dummy } Component { id: controlCenter FloatingWindow { id: win property alias active: cc.active property alias navExpanded: cc.navExpanded color: Colours.tPalette.m3surface onVisibleChanged: { if (!visible) destroy(); } implicitWidth: cc.implicitWidth implicitHeight: cc.implicitHeight minimumSize.width: implicitWidth minimumSize.height: implicitHeight maximumSize.width: implicitWidth maximumSize.height: implicitHeight title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1)) ControlCenter { id: cc function close(): void { win.destroy(); } anchors.fill: parent screen: win.screen floating: true } Behavior on color { CAnim {} } } } } ================================================ FILE: modules/controlcenter/WindowTitle.qml ================================================ import qs.components import qs.services import qs.config import Quickshell import QtQuick StyledRect { id: root required property ShellScreen screen required property Session session implicitHeight: text.implicitHeight + Appearance.padding.normal color: Colours.tPalette.m3surfaceContainer StyledText { id: text anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom text: qsTr("Caelestia Settings - %1").arg(root.session.active) font.capitalization: Font.Capitalize font.pointSize: Appearance.font.size.larger font.weight: 500 } Item { anchors.right: parent.right anchors.top: parent.top anchors.margins: Appearance.padding.normal implicitWidth: implicitHeight implicitHeight: closeIcon.implicitHeight + Appearance.padding.small StateLayer { function onClicked(): void { QsWindow.window.destroy(); } radius: Appearance.rounding.full } MaterialIcon { id: closeIcon anchors.centerIn: parent text: "close" } } } ================================================ FILE: modules/controlcenter/appearance/AppearancePane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import "./sections" import qs.components import qs.components.controls import qs.components.containers import qs.config import QtQuick import QtQuick.Layouts Item { id: root required property Session session property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" property real fontSizeScale: Config.appearance.font.size.scale ?? 1 property real paddingScale: Config.appearance.padding.scale ?? 1 property real roundingScale: Config.appearance.rounding.scale ?? 1 property real spacingScale: Config.appearance.spacing.scale ?? 1 property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false property real transparencyBase: Config.appearance.transparency.base ?? 0.85 property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4 property real borderRounding: Config.border.rounding ?? 1 property real borderThickness: Config.border.thickness ?? 1 property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false property real desktopClockScale: Config.background.desktopClock.scale ?? 1 property string desktopClockPosition: Config.background.desktopClock.position ?? "bottom-right" property bool desktopClockShadowEnabled: Config.background.desktopClock.shadow.enabled ?? true property real desktopClockShadowOpacity: Config.background.desktopClock.shadow.opacity ?? 0.7 property real desktopClockShadowBlur: Config.background.desktopClock.shadow.blur ?? 0.4 property bool desktopClockBackgroundEnabled: Config.background.desktopClock.background.enabled ?? false property real desktopClockBackgroundOpacity: Config.background.desktopClock.background.opacity ?? 0.7 property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false property bool backgroundEnabled: Config.background.enabled ?? true property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true property bool visualiserEnabled: Config.background.visualiser.enabled ?? false property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true property real visualiserRounding: Config.background.visualiser.rounding ?? 1 property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 function saveConfig() { Config.appearance.anim.durations.scale = root.animDurationsScale; Config.appearance.font.family.material = root.fontFamilyMaterial; Config.appearance.font.family.mono = root.fontFamilyMono; Config.appearance.font.family.sans = root.fontFamilySans; Config.appearance.font.size.scale = root.fontSizeScale; Config.appearance.padding.scale = root.paddingScale; Config.appearance.rounding.scale = root.roundingScale; Config.appearance.spacing.scale = root.spacingScale; Config.appearance.transparency.enabled = root.transparencyEnabled; Config.appearance.transparency.base = root.transparencyBase; Config.appearance.transparency.layers = root.transparencyLayers; Config.background.desktopClock.enabled = root.desktopClockEnabled; Config.background.enabled = root.backgroundEnabled; Config.background.desktopClock.scale = root.desktopClockScale; Config.background.desktopClock.position = root.desktopClockPosition; Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; Config.background.desktopClock.invertColors = root.desktopClockInvertColors; Config.background.wallpaperEnabled = root.wallpaperEnabled; Config.background.visualiser.enabled = root.visualiserEnabled; Config.background.visualiser.autoHide = root.visualiserAutoHide; Config.background.visualiser.rounding = root.visualiserRounding; Config.background.visualiser.spacing = root.visualiserSpacing; Config.border.rounding = root.borderRounding; Config.border.thickness = root.borderThickness; Config.save(); } anchors.fill: parent Component { id: appearanceRightContentComponent Item { id: rightAppearanceFlickable ColumnLayout { id: contentLayout anchors.fill: parent spacing: 0 StyledText { Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: Appearance.spacing.normal text: qsTr("Wallpaper") font.pointSize: Appearance.font.size.extraLarge font.weight: 600 } Loader { id: wallpaperLoader Layout.fillWidth: true Layout.fillHeight: true Layout.bottomMargin: -Appearance.padding.large * 2 asynchronous: true active: { const isActive = root.session.activeIndex === 3; const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; const splitLayout = root.children[0]; const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null; const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent); return shouldActivate; } onStatusChanged: { if (status === Loader.Error) { console.error("[AppearancePane] Wallpaper loader error!"); } } sourceComponent: WallpaperGrid { session: root.session } } } } } SplitPaneLayout { anchors.fill: parent leftContent: Component { StyledFlickable { id: sidebarFlickable readonly property var rootPane: root flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: sidebarFlickable } ColumnLayout { id: sidebarLayout readonly property var rootPane: sidebarFlickable.rootPane readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.small RowLayout { spacing: Appearance.spacing.smaller StyledText { text: qsTr("Appearance") font.pointSize: Appearance.font.size.large font.weight: 500 } Item { Layout.fillWidth: true } IconButton { icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more" type: IconButton.Text label.animate: true onClicked: { const shouldExpand = !sidebarLayout.allSectionsExpanded; themeModeSection.expanded = shouldExpand; colorVariantSection.expanded = shouldExpand; colorSchemeSection.expanded = shouldExpand; animationsSection.expanded = shouldExpand; fontsSection.expanded = shouldExpand; scalesSection.expanded = shouldExpand; transparencySection.expanded = shouldExpand; borderSection.expanded = shouldExpand; backgroundSection.expanded = shouldExpand; } } } ThemeModeSection { id: themeModeSection } ColorVariantSection { id: colorVariantSection } ColorSchemeSection { id: colorSchemeSection } AnimationsSection { id: animationsSection rootPane: sidebarFlickable.rootPane } FontsSection { id: fontsSection rootPane: sidebarFlickable.rootPane } ScalesSection { id: scalesSection rootPane: sidebarFlickable.rootPane } TransparencySection { id: transparencySection rootPane: sidebarFlickable.rootPane } BorderSection { id: borderSection rootPane: sidebarFlickable.rootPane } BackgroundSection { id: backgroundSection rootPane: sidebarFlickable.rootPane } } } } rightContent: appearanceRightContentComponent } } ================================================ FILE: modules/controlcenter/appearance/sections/AnimationsSection.qml ================================================ pragma ComponentBehavior: Bound import "../../components" import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts CollapsibleSection { id: root required property var rootPane title: qsTr("Animations") showBackground: true SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Animation duration scale") value: root.rootPane.animDurationsScale from: 0.1 to: 5.0 decimals: 1 suffix: "×" validator: DoubleValidator { bottom: 0.1 top: 5.0 } onValueModified: newValue => { root.rootPane.animDurationsScale = newValue; root.rootPane.saveConfig(); } } } } ================================================ FILE: modules/controlcenter/appearance/sections/BackgroundSection.qml ================================================ pragma ComponentBehavior: Bound import "../../components" import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts CollapsibleSection { id: root required property var rootPane title: qsTr("Background") showBackground: true SwitchRow { label: qsTr("Background enabled") checked: root.rootPane.backgroundEnabled onToggled: checked => { root.rootPane.backgroundEnabled = checked; root.rootPane.saveConfig(); } } SwitchRow { label: qsTr("Wallpaper enabled") checked: root.rootPane.wallpaperEnabled onToggled: checked => { root.rootPane.wallpaperEnabled = checked; root.rootPane.saveConfig(); } } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Desktop Clock") font.pointSize: Appearance.font.size.larger font.weight: 500 } SwitchRow { label: qsTr("Desktop Clock enabled") checked: root.rootPane.desktopClockEnabled onToggled: checked => { root.rootPane.desktopClockEnabled = checked; root.rootPane.saveConfig(); } } SectionContainer { id: posContainer readonly property var pos: (root.rootPane.desktopClockPosition || "top-left").split('-') readonly property string currentV: pos[0] readonly property string currentH: pos[1] function updateClockPos(v, h) { root.rootPane.desktopClockPosition = v + "-" + h; root.rootPane.saveConfig(); } contentSpacing: Appearance.spacing.small z: 1 StyledText { text: qsTr("Positioning") font.pointSize: Appearance.font.size.larger font.weight: 500 } SplitButtonRow { label: qsTr("Vertical Position") enabled: root.rootPane.desktopClockEnabled menuItems: [ MenuItem { property string val: "top" text: qsTr("Top") icon: "vertical_align_top" }, MenuItem { property string val: "middle" text: qsTr("Middle") icon: "vertical_align_center" }, MenuItem { property string val: "bottom" text: qsTr("Bottom") icon: "vertical_align_bottom" } ] Component.onCompleted: { for (let i = 0; i < menuItems.length; i++) { if (menuItems[i].val === posContainer.currentV) active = menuItems[i]; } } // The signal from SplitButtonRow onSelected: item => posContainer.updateClockPos(item.val, posContainer.currentH) } SplitButtonRow { label: qsTr("Horizontal Position") enabled: root.rootPane.desktopClockEnabled expandedZ: 99 menuItems: [ MenuItem { property string val: "left" text: qsTr("Left") icon: "align_horizontal_left" }, MenuItem { property string val: "center" text: qsTr("Center") icon: "align_horizontal_center" }, MenuItem { property string val: "right" text: qsTr("Right") icon: "align_horizontal_right" } ] Component.onCompleted: { for (let i = 0; i < menuItems.length; i++) { if (menuItems[i].val === posContainer.currentH) active = menuItems[i]; } } onSelected: item => posContainer.updateClockPos(posContainer.currentV, item.val) } } SwitchRow { label: qsTr("Invert colors") checked: root.rootPane.desktopClockInvertColors onToggled: checked => { root.rootPane.desktopClockInvertColors = checked; root.rootPane.saveConfig(); } } SectionContainer { contentSpacing: Appearance.spacing.small StyledText { text: qsTr("Shadow") font.pointSize: Appearance.font.size.larger font.weight: 500 } SwitchRow { label: qsTr("Enabled") checked: root.rootPane.desktopClockShadowEnabled onToggled: checked => { root.rootPane.desktopClockShadowEnabled = checked; root.rootPane.saveConfig(); } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Opacity") value: root.rootPane.desktopClockShadowOpacity * 100 from: 0 to: 100 suffix: "%" validator: IntValidator { bottom: 0 top: 100 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootPane.desktopClockShadowOpacity = newValue / 100; root.rootPane.saveConfig(); } } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Blur") value: root.rootPane.desktopClockShadowBlur * 100 from: 0 to: 100 suffix: "%" validator: IntValidator { bottom: 0 top: 100 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootPane.desktopClockShadowBlur = newValue / 100; root.rootPane.saveConfig(); } } } } SectionContainer { contentSpacing: Appearance.spacing.small StyledText { text: qsTr("Background") font.pointSize: Appearance.font.size.larger font.weight: 500 } SwitchRow { label: qsTr("Enabled") checked: root.rootPane.desktopClockBackgroundEnabled onToggled: checked => { root.rootPane.desktopClockBackgroundEnabled = checked; root.rootPane.saveConfig(); } } SwitchRow { label: qsTr("Blur enabled") checked: root.rootPane.desktopClockBackgroundBlur onToggled: checked => { root.rootPane.desktopClockBackgroundBlur = checked; root.rootPane.saveConfig(); } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Opacity") value: root.rootPane.desktopClockBackgroundOpacity * 100 from: 0 to: 100 suffix: "%" validator: IntValidator { bottom: 0 top: 100 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootPane.desktopClockBackgroundOpacity = newValue / 100; root.rootPane.saveConfig(); } } } } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Visualiser") font.pointSize: Appearance.font.size.larger font.weight: 500 } SwitchRow { label: qsTr("Visualiser enabled") checked: root.rootPane.visualiserEnabled onToggled: checked => { root.rootPane.visualiserEnabled = checked; root.rootPane.saveConfig(); } } SwitchRow { label: qsTr("Visualiser auto hide") checked: root.rootPane.visualiserAutoHide onToggled: checked => { root.rootPane.visualiserAutoHide = checked; root.rootPane.saveConfig(); } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Visualiser rounding") value: root.rootPane.visualiserRounding from: 0 to: 10 stepSize: 1 validator: IntValidator { bottom: 0 top: 10 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootPane.visualiserRounding = Math.round(newValue); root.rootPane.saveConfig(); } } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Visualiser spacing") value: root.rootPane.visualiserSpacing from: 0 to: 2 validator: DoubleValidator { bottom: 0 top: 2 } onValueModified: newValue => { root.rootPane.visualiserSpacing = newValue; root.rootPane.saveConfig(); } } } } ================================================ FILE: modules/controlcenter/appearance/sections/BorderSection.qml ================================================ pragma ComponentBehavior: Bound import "../../components" import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts CollapsibleSection { id: root required property var rootPane title: qsTr("Border") showBackground: true SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Border rounding") value: root.rootPane.borderRounding from: 0.1 to: 100 decimals: 1 suffix: "px" validator: DoubleValidator { bottom: 0.1 top: 100 } onValueModified: newValue => { root.rootPane.borderRounding = newValue; root.rootPane.saveConfig(); } } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Border thickness") value: root.rootPane.borderThickness from: 0 to: 100 decimals: 1 suffix: "px" validator: DoubleValidator { bottom: 0.1 top: 100 } onValueModified: newValue => { root.rootPane.borderThickness = newValue; root.rootPane.saveConfig(); } } } } ================================================ FILE: modules/controlcenter/appearance/sections/ColorSchemeSection.qml ================================================ pragma ComponentBehavior: Bound import "../../../launcher/services" import qs.components import qs.components.controls import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Layouts CollapsibleSection { title: qsTr("Color scheme") description: qsTr("Available color schemes") showBackground: true ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small / 2 Repeater { model: Schemes.list delegate: StyledRect { id: schemeDelegate required property var modelData Layout.fillWidth: true readonly property string schemeKey: `${modelData.name} ${modelData.flavour}` readonly property bool isCurrent: schemeKey === Schemes.currentScheme color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { const name = schemeDelegate.modelData.name; const flavour = schemeDelegate.modelData.flavour; const schemeKey = `${name} ${flavour}`; Schemes.currentScheme = schemeKey; Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); Qt.callLater(() => { reloadTimer.restart(); }); } } Timer { id: reloadTimer interval: 300 onTriggered: { Schemes.reload(); } } RowLayout { id: schemeRow anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledRect { id: preview Layout.alignment: Qt.AlignVCenter border.width: 1 border.color: Qt.alpha(`#${schemeDelegate.modelData.colours?.outline}`, 0.5) color: `#${schemeDelegate.modelData.colours?.surface}` radius: Appearance.rounding.full implicitWidth: iconPlaceholder.implicitWidth implicitHeight: iconPlaceholder.implicitWidth MaterialIcon { id: iconPlaceholder visible: false text: "circle" font.pointSize: Appearance.font.size.large } Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right implicitWidth: parent.implicitWidth / 2 clip: true StyledRect { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right implicitWidth: preview.implicitWidth color: `#${schemeDelegate.modelData.colours?.primary}` radius: Appearance.rounding.full } } } Column { Layout.fillWidth: true spacing: 0 StyledText { text: schemeDelegate.modelData.flavour ?? "" font.pointSize: Appearance.font.size.normal } StyledText { text: schemeDelegate.modelData.name ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight anchors.left: parent.left anchors.right: parent.right } } Loader { asynchronous: true active: schemeDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.large } } } implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } } ================================================ FILE: modules/controlcenter/appearance/sections/ColorVariantSection.qml ================================================ pragma ComponentBehavior: Bound import "../../../launcher/services" import qs.components import qs.components.controls import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Layouts CollapsibleSection { title: qsTr("Color variant") description: qsTr("Material theme variant") showBackground: true ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small / 2 Repeater { model: M3Variants.list delegate: StyledRect { id: variantDelegate required property var modelData Layout.fillWidth: true color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { const variant = variantDelegate.modelData.variant; Schemes.currentVariant = variant; Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); Qt.callLater(() => { reloadTimer.restart(); }); } } Timer { id: reloadTimer interval: 300 onTriggered: { Schemes.reload(); } } RowLayout { id: variantRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal MaterialIcon { text: variantDelegate.modelData.icon font.pointSize: Appearance.font.size.large fill: variantDelegate.modelData.variant === Schemes.currentVariant ? 1 : 0 } StyledText { Layout.fillWidth: true text: variantDelegate.modelData.name font.weight: variantDelegate.modelData.variant === Schemes.currentVariant ? 500 : 400 } MaterialIcon { visible: variantDelegate.modelData.variant === Schemes.currentVariant text: "check" color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large } } implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } } ================================================ FILE: modules/controlcenter/appearance/sections/FontsSection.qml ================================================ pragma ComponentBehavior: Bound import "../../components" import qs.components import qs.components.controls import qs.components.containers import qs.services import qs.config import QtQuick import QtQuick.Layouts CollapsibleSection { id: root required property var rootPane title: qsTr("Fonts") showBackground: true CollapsibleSection { id: sansFontSection title: qsTr("Sans-serif font family") expanded: true showBackground: true nested: true Loader { Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 asynchronous: true active: sansFontSection.expanded sourceComponent: StyledListView { id: sansFontList property alias contentHeight: sansFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { flickable: sansFontList } delegate: StyledRect { id: sansDelegate required property string modelData required property int index readonly property bool isCurrent: modelData === root.rootPane.fontFamilySans width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { root.rootPane.fontFamilySans = sansDelegate.modelData; root.rootPane.saveConfig(); } } RowLayout { id: fontFamilySansRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledText { text: sansDelegate.modelData font.pointSize: Appearance.font.size.normal } Item { Layout.fillWidth: true } Loader { asynchronous: true active: sansDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.large } } } implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { id: monoFontSection title: qsTr("Monospace font family") expanded: false showBackground: true nested: true Loader { Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 asynchronous: true active: monoFontSection.expanded sourceComponent: StyledListView { id: monoFontList property alias contentHeight: monoFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { flickable: monoFontList } delegate: StyledRect { id: monoDelegate required property string modelData required property int index readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMono width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { root.rootPane.fontFamilyMono = monoDelegate.modelData; root.rootPane.saveConfig(); } } RowLayout { id: fontFamilyMonoRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledText { text: monoDelegate.modelData font.pointSize: Appearance.font.size.normal } Item { Layout.fillWidth: true } Loader { asynchronous: true active: monoDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.large } } } implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { id: materialFontSection title: qsTr("Material font family") expanded: false showBackground: true nested: true Loader { id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 asynchronous: true active: materialFontSection.expanded sourceComponent: StyledListView { id: materialFontList property alias contentHeight: materialFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) StyledScrollBar.vertical: StyledScrollBar { flickable: materialFontList } delegate: StyledRect { id: materialDelegate required property string modelData required property int index readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMaterial width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary StateLayer { function onClicked(): void { root.rootPane.fontFamilyMaterial = materialDelegate.modelData; root.rootPane.saveConfig(); } } RowLayout { id: fontFamilyMaterialRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledText { text: materialDelegate.modelData font.pointSize: Appearance.font.size.normal } Item { Layout.fillWidth: true } Loader { asynchronous: true active: materialDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.large } } } implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Font size scale") value: root.rootPane.fontSizeScale from: 0.7 to: 1.5 decimals: 2 suffix: "×" validator: DoubleValidator { bottom: 0.7 top: 1.5 } onValueModified: newValue => { root.rootPane.fontSizeScale = newValue; root.rootPane.saveConfig(); } } } } ================================================ FILE: modules/controlcenter/appearance/sections/ScalesSection.qml ================================================ pragma ComponentBehavior: Bound import "../../components" import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts CollapsibleSection { id: root required property var rootPane title: qsTr("Scales") showBackground: true SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Padding scale") value: root.rootPane.paddingScale from: 0.5 to: 2.0 decimals: 1 suffix: "×" validator: DoubleValidator { bottom: 0.5 top: 2.0 } onValueModified: newValue => { root.rootPane.paddingScale = newValue; root.rootPane.saveConfig(); } } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Rounding scale") value: root.rootPane.roundingScale from: 0.1 to: 5.0 decimals: 1 suffix: "×" validator: DoubleValidator { bottom: 0.1 top: 5.0 } onValueModified: newValue => { root.rootPane.roundingScale = newValue; root.rootPane.saveConfig(); } } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Spacing scale") value: root.rootPane.spacingScale from: 0.1 to: 2.0 decimals: 1 suffix: "×" validator: DoubleValidator { bottom: 0.1 top: 2.0 } onValueModified: newValue => { root.rootPane.spacingScale = newValue; root.rootPane.saveConfig(); } } } } ================================================ FILE: modules/controlcenter/appearance/sections/ThemeModeSection.qml ================================================ pragma ComponentBehavior: Bound import qs.components.controls import qs.services import QtQuick CollapsibleSection { title: qsTr("Theme mode") description: qsTr("Light or dark theme") showBackground: true SwitchRow { label: qsTr("Dark mode") checked: !Colours.currentLight onToggled: checked => { Colours.setMode(checked ? "dark" : "light"); } } } ================================================ FILE: modules/controlcenter/appearance/sections/TransparencySection.qml ================================================ pragma ComponentBehavior: Bound import "../../components" import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts CollapsibleSection { id: root required property var rootPane title: qsTr("Transparency") showBackground: true SwitchRow { label: qsTr("Transparency enabled") checked: root.rootPane.transparencyEnabled onToggled: checked => { root.rootPane.transparencyEnabled = checked; root.rootPane.saveConfig(); } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Transparency base") value: root.rootPane.transparencyBase * 100 from: 0 to: 100 suffix: "%" validator: IntValidator { bottom: 0 top: 100 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootPane.transparencyBase = newValue / 100; root.rootPane.saveConfig(); } } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Transparency layers") value: root.rootPane.transparencyLayers * 100 from: 0 to: 100 suffix: "%" validator: IntValidator { bottom: 0 top: 100 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootPane.transparencyLayers = newValue / 100; root.rootPane.saveConfig(); } } } } ================================================ FILE: modules/controlcenter/audio/AudioPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.components.containers import qs.services import qs.config import QtQuick import QtQuick.Layouts Item { id: root required property Session session anchors.fill: parent SplitPaneLayout { anchors.fill: parent leftContent: Component { StyledFlickable { id: leftAudioFlickable flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height StyledScrollBar.vertical: StyledScrollBar { flickable: leftAudioFlickable } ColumnLayout { id: leftContent anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.normal RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { text: qsTr("Audio") font.pointSize: Appearance.font.size.large font.weight: 500 } Item { Layout.fillWidth: true } } CollapsibleSection { id: outputDevicesSection Layout.fillWidth: true title: qsTr("Output devices") expanded: true ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.small StyledText { text: qsTr("Devices (%1)").arg(Audio.sinks.length) font.pointSize: Appearance.font.size.normal font.weight: 500 } } StyledText { Layout.fillWidth: true text: qsTr("All available output devices") color: Colours.palette.m3outline } Repeater { Layout.fillWidth: true model: Audio.sinks delegate: StyledRect { id: outputDeviceDelegate required property var modelData Layout.fillWidth: true color: Audio.sink?.id === outputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { function onClicked(): void { Audio.setAudioSink(outputDeviceDelegate.modelData); } } RowLayout { id: outputRowLayout anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal MaterialIcon { text: Audio.sink?.id === outputDeviceDelegate.modelData.id ? "speaker" : "speaker_group" font.pointSize: Appearance.font.size.large fill: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 1 : 0 } StyledText { Layout.fillWidth: true elide: Text.ElideRight maximumLineCount: 1 text: outputDeviceDelegate.modelData.description || qsTr("Unknown") font.weight: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 500 : 400 } } implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { id: inputDevicesSection Layout.fillWidth: true title: qsTr("Input devices") expanded: true ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.small StyledText { text: qsTr("Devices (%1)").arg(Audio.sources.length) font.pointSize: Appearance.font.size.normal font.weight: 500 } } StyledText { Layout.fillWidth: true text: qsTr("All available input devices") color: Colours.palette.m3outline } Repeater { Layout.fillWidth: true model: Audio.sources delegate: StyledRect { id: inputDeviceDelegate required property var modelData Layout.fillWidth: true color: Audio.source?.id === inputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { function onClicked(): void { Audio.setAudioSource(inputDeviceDelegate.modelData); } } RowLayout { id: inputRowLayout anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal MaterialIcon { text: "mic" font.pointSize: Appearance.font.size.large fill: Audio.source?.id === inputDeviceDelegate.modelData.id ? 1 : 0 } StyledText { Layout.fillWidth: true elide: Text.ElideRight maximumLineCount: 1 text: inputDeviceDelegate.modelData.description || qsTr("Unknown") font.weight: Audio.source?.id === inputDeviceDelegate.modelData.id ? 500 : 400 } } implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } } } } } rightContent: Component { StyledFlickable { id: rightAudioFlickable flickableDirection: Flickable.VerticalFlick contentHeight: contentLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: rightAudioFlickable } ColumnLayout { id: contentLayout anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: Appearance.spacing.normal SettingsHeader { icon: "volume_up" title: qsTr("Audio Settings") } SectionHeader { title: qsTr("Output volume") description: qsTr("Control the volume of your output device") } SectionContainer { contentSpacing: Appearance.spacing.normal ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { text: qsTr("Volume") font.pointSize: Appearance.font.size.normal font.weight: 500 } Item { Layout.fillWidth: true } StyledInputField { id: outputVolumeInput Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 top: 100 } enabled: !Audio.muted Component.onCompleted: { text = Math.round(Audio.volume * 100).toString(); } Connections { function onVolumeChanged() { if (!outputVolumeInput.hasFocus) { outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); } } target: Audio } onTextEdited: text => { if (hasFocus) { const val = parseInt(text); if (!isNaN(val) && val >= 0 && val <= 100) { Audio.setVolume(val / 100); } } } onEditingFinished: { const val = parseInt(text); if (isNaN(val) || val < 0 || val > 100) { text = Math.round(Audio.volume * 100).toString(); } } } StyledText { text: "%" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal opacity: Audio.muted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { function onClicked(): void { if (Audio.sink?.audio) { Audio.sink.audio.muted = !Audio.sink.audio.muted; } } } MaterialIcon { id: muteIcon anchors.centerIn: parent text: Audio.muted ? "volume_off" : "volume_up" color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } StyledSlider { id: outputVolumeSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 value: Audio.volume enabled: !Audio.muted opacity: enabled ? 1 : 0.5 onMoved: { Audio.setVolume(value); if (!outputVolumeInput.hasFocus) { outputVolumeInput.text = Math.round(value * 100).toString(); } } } } } SectionHeader { title: qsTr("Input volume") description: qsTr("Control the volume of your input device") } SectionContainer { contentSpacing: Appearance.spacing.normal ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { text: qsTr("Volume") font.pointSize: Appearance.font.size.normal font.weight: 500 } Item { Layout.fillWidth: true } StyledInputField { id: inputVolumeInput Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 top: 100 } enabled: !Audio.sourceMuted Component.onCompleted: { text = Math.round(Audio.sourceVolume * 100).toString(); } Connections { function onSourceVolumeChanged() { if (!inputVolumeInput.hasFocus) { inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); } } target: Audio } onTextEdited: text => { if (hasFocus) { const val = parseInt(text); if (!isNaN(val) && val >= 0 && val <= 100) { Audio.setSourceVolume(val / 100); } } } onEditingFinished: { const val = parseInt(text); if (isNaN(val) || val < 0 || val > 100) { text = Math.round(Audio.sourceVolume * 100).toString(); } } } StyledText { text: "%" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal opacity: Audio.sourceMuted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { function onClicked(): void { if (Audio.source?.audio) { Audio.source.audio.muted = !Audio.source.audio.muted; } } } MaterialIcon { id: muteInputIcon anchors.centerIn: parent text: "mic_off" color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } StyledSlider { id: inputVolumeSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 value: Audio.sourceVolume enabled: !Audio.sourceMuted opacity: enabled ? 1 : 0.5 onMoved: { Audio.setSourceVolume(value); if (!inputVolumeInput.hasFocus) { inputVolumeInput.text = Math.round(value * 100).toString(); } } } } } SectionHeader { title: qsTr("Applications") description: qsTr("Control volume for individual applications") } SectionContainer { contentSpacing: Appearance.spacing.normal ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small Repeater { model: Audio.streams Layout.fillWidth: true delegate: ColumnLayout { id: streamDelegate required property var modelData required property int index Layout.fillWidth: true spacing: Appearance.spacing.smaller RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal MaterialIcon { text: "apps" font.pointSize: Appearance.font.size.normal fill: 0 } StyledText { Layout.fillWidth: true elide: Text.ElideRight maximumLineCount: 1 text: Audio.getStreamName(streamDelegate.modelData) font.pointSize: Appearance.font.size.normal font.weight: 500 } StyledInputField { id: streamVolumeInput Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 top: 100 } enabled: !Audio.getStreamMuted(streamDelegate.modelData) Component.onCompleted: { text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString(); } Connections { function onAudioChanged() { if (!streamVolumeInput.hasFocus && streamDelegate.modelData?.audio) { streamVolumeInput.text = Math.round(streamDelegate.modelData.audio.volume * 100).toString(); } } target: streamDelegate.modelData } onTextEdited: text => { if (hasFocus) { const val = parseInt(text); if (!isNaN(val) && val >= 0 && val <= 100) { Audio.setStreamVolume(streamDelegate.modelData, val / 100); } } } onEditingFinished: { const val = parseInt(text); if (isNaN(val) || val < 0 || val > 100) { text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString(); } } } StyledText { text: "%" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal opacity: Audio.getStreamMuted(streamDelegate.modelData) ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { function onClicked(): void { Audio.setStreamMuted(streamDelegate.modelData, !Audio.getStreamMuted(streamDelegate.modelData)); } } MaterialIcon { id: streamMuteIcon anchors.centerIn: parent text: Audio.getStreamMuted(streamDelegate.modelData) ? "volume_off" : "volume_up" color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } StyledSlider { id: streamSlider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 value: Audio.getStreamVolume(streamDelegate.modelData) enabled: !Audio.getStreamMuted(streamDelegate.modelData) opacity: enabled ? 1 : 0.5 onMoved: { Audio.setStreamVolume(streamDelegate.modelData, value); if (!streamVolumeInput.hasFocus) { streamVolumeInput.text = Math.round(value * 100).toString(); } } Connections { function onAudioChanged() { if (streamDelegate.modelData?.audio) { streamSlider.value = streamDelegate.modelData.audio.volume; } } target: streamDelegate.modelData } } } } StyledText { Layout.fillWidth: true visible: Audio.streams.length === 0 text: qsTr("No applications currently playing audio") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small horizontalAlignment: Text.AlignHCenter } } } } } } } } ================================================ FILE: modules/controlcenter/bluetooth/BtPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import "." import qs.components.controls import qs.components.containers import QtQuick SplitPaneWithDetails { id: root required property Session session anchors.fill: parent activeItem: session.bt.active paneIdGenerator: function (item) { return item ? (item.address || "") : ""; } leftContent: Component { StyledFlickable { id: leftFlickable flickableDirection: Flickable.VerticalFlick contentHeight: deviceList.height StyledScrollBar.vertical: StyledScrollBar { flickable: leftFlickable } DeviceList { id: deviceList anchors.left: parent.left anchors.right: parent.right session: root.session } } } rightDetailsComponent: Component { Details { session: root.session } } rightSettingsComponent: Component { StyledFlickable { id: settingsFlickable flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height StyledScrollBar.vertical: StyledScrollBar { flickable: settingsFlickable } Settings { id: settingsInner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session } } } } ================================================ FILE: modules/controlcenter/bluetooth/Details.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.components.effects import qs.components.containers import qs.services import qs.config import qs.utils import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts StyledFlickable { id: root required property Session session readonly property BluetoothDevice device: session.bt.active flickableDirection: Flickable.VerticalFlick contentHeight: detailsWrapper.height StyledScrollBar.vertical: StyledScrollBar { flickable: root } Item { id: detailsWrapper anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top implicitHeight: details.implicitHeight DeviceDetails { id: details anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session device: root.device headerComponent: Component { SettingsHeader { icon: Icons.getBluetoothIcon(root.device?.icon ?? "") title: root.device?.name ?? "" } } sections: [ Component { ColumnLayout { spacing: Appearance.spacing.normal StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Connection status") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("Connection settings for this device") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: deviceStatus anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.larger Toggle { label: qsTr("Connected") checked: root.device?.connected ?? false toggle.onToggled: root.device.connected = checked } Toggle { label: qsTr("Paired") checked: root.device?.paired ?? false toggle.onToggled: { if (root.device.paired) root.device.forget(); else root.device.pair(); } } Toggle { label: qsTr("Blocked") checked: root.device?.blocked ?? false toggle.onToggled: root.device.blocked = checked } } } } }, Component { ColumnLayout { spacing: Appearance.spacing.normal StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Device properties") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("Additional settings") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: deviceProps anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.larger RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.small Item { id: renameDevice Layout.fillWidth: true Layout.rightMargin: Appearance.spacing.small implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight states: State { name: "editingDeviceName" when: root.session.bt.editingDeviceName AnchorChanges { target: deviceNameEdit anchors.top: renameDevice.top } PropertyChanges { renameDevice.implicitHeight: deviceNameEdit.implicitHeight renameLabel.opacity: 0 deviceNameEdit.padding: Appearance.padding.normal } } transitions: Transition { AnchorAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } Anim { properties: "implicitHeight,opacity,padding" } } StyledText { id: renameLabel anchors.left: parent.left text: qsTr("Device name") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledTextField { id: deviceNameEdit anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal text: root.device?.name ?? "" readOnly: !root.session.bt.editingDeviceName onAccepted: { root.session.bt.editingDeviceName = false; root.device.name = text; } leftPadding: Appearance.padding.normal rightPadding: Appearance.padding.normal background: StyledRect { radius: Appearance.rounding.small border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingDeviceName ? 1 : 0 Behavior on border.color { CAnim {} } Behavior on opacity { Anim {} } } Behavior on anchors.leftMargin { Anim {} } } } StyledRect { implicitWidth: implicitHeight implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.small color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingDeviceName ? 1 : 0 scale: root.session.bt.editingDeviceName ? 1 : 0.5 StateLayer { function onClicked(): void { root.session.bt.editingDeviceName = false; deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); } color: Colours.palette.m3onSecondaryContainer disabled: !root.session.bt.editingDeviceName } MaterialIcon { id: cancelEditIcon anchors.centerIn: parent animate: true text: "cancel" color: Colours.palette.m3onSecondaryContainer } Behavior on opacity { Anim {} } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } StyledRect { implicitWidth: implicitHeight implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { function onClicked(): void { root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; if (root.session.bt.editingDeviceName) deviceNameEdit.forceActiveFocus(); else deviceNameEdit.accepted(); } color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { id: editIcon anchors.centerIn: parent animate: true text: root.session.bt.editingDeviceName ? "check_circle" : "edit" color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } Behavior on radius { Anim {} } } } Toggle { label: qsTr("Trusted") checked: root.device?.trusted ?? false toggle.onToggled: root.device.trusted = checked } Toggle { label: qsTr("Wake allowed") checked: root.device?.wakeAllowed ?? false toggle.onToggled: root.device.wakeAllowed = checked } } } } }, Component { ColumnLayout { spacing: Appearance.spacing.normal StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Device information") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("Information about this device") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: deviceInfo anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.small / 2 StyledText { text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") } RowLayout { id: batteryPercent Layout.topMargin: Appearance.spacing.small / 2 Layout.fillWidth: true Layout.preferredHeight: Appearance.padding.smaller spacing: Appearance.spacing.small / 2 StyledRect { Layout.fillWidth: true Layout.fillHeight: true radius: Appearance.rounding.full color: Colours.palette.m3secondaryContainer StyledRect { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: parent.height * 0.25 implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 radius: Appearance.rounding.full color: Colours.palette.m3primary } } } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Dbus path") } StyledText { text: root.device?.dbusPath ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("MAC address") } StyledText { text: root.device?.address ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Bonded") } StyledText { text: root.device?.bonded ? qsTr("Yes") : qsTr("No") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("System name") } StyledText { text: root.device?.deviceName ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } } } } } ] } } ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top anchors.bottomMargin: Appearance.padding.normal Repeater { id: fabMenu model: ListModel { ListElement { name: "trust" icon: "handshake" } ListElement { name: "block" icon: "block" } ListElement { name: "pair" icon: "missing_controller" } ListElement { name: "connect" icon: "bluetooth_connected" } } StyledClippingRect { id: fabMenuItem required property var modelData required property int index Layout.alignment: Qt.AlignRight implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2 radius: Appearance.rounding.full color: Colours.palette.m3primaryContainer opacity: 0 states: State { name: "visible" when: root.session.bt.fabMenuOpen PropertyChanges { fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2 fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } } transitions: [ Transition { to: "visible" SequentialAnimation { PauseAnimation { duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { property: "opacity" duration: Appearance.anim.durations.small } } } }, Transition { from: "visible" SequentialAnimation { PauseAnimation { duration: fabMenuItem.index * Appearance.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { property: "opacity" duration: Appearance.anim.durations.small } } } } ] StateLayer { function onClicked(): void { root.session.bt.fabMenuOpen = false; const name = fabMenuItem.modelData.name; if (fabMenuItem.modelData.name !== "pair") root.device[`${name}ed`] = !root.device[`${name}ed`]; else if (root.device.paired) root.device.forget(); else root.device.pair(); } } RowLayout { id: fabMenuItemInner anchors.centerIn: parent spacing: Appearance.spacing.normal opacity: 0 MaterialIcon { text: fabMenuItem.modelData.icon color: Colours.palette.m3onPrimaryContainer fill: 1 } StyledText { animate: true text: (root.device && root.device[`${fabMenuItem.modelData.name}ed`] ? fabMenuItem.modelData.name === "connect" ? "dis" : "un" : "") + fabMenuItem.modelData.name color: Colours.palette.m3onPrimaryContainer font.capitalization: Font.Capitalize Layout.preferredWidth: implicitWidth Behavior on Layout.preferredWidth { Anim { duration: Appearance.anim.durations.small } } } } } } } Item { id: fabRoot x: root.contentX + root.width - width y: root.contentY + root.height - height width: 64 height: 64 z: 10000 StyledRect { id: fabBg anchors.right: parent.right anchors.top: parent.top implicitWidth: 64 implicitHeight: 64 radius: Appearance.rounding.normal color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer states: State { name: "expanded" when: root.session.bt.fabMenuOpen PropertyChanges { fabBg.implicitWidth: 48 fabBg.implicitHeight: 48 fabBg.radius: 48 / 2 fab.font.pointSize: Appearance.font.size.larger } } transitions: Transition { Anim { properties: "implicitWidth,implicitHeight" duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { properties: "radius,font.pointSize" } } Elevation { anchors.fill: parent radius: parent.radius z: -1 level: fabState.containsMouse && !fabState.pressed ? 4 : 3 } StateLayer { id: fabState function onClicked(): void { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer } MaterialIcon { id: fab anchors.centerIn: parent animate: true text: root.session.bt.fabMenuOpen ? "close" : "settings" color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer font.pointSize: Appearance.font.size.large fill: 1 } } } component Toggle: RowLayout { required property string label property alias checked: toggle.checked property alias toggle: toggle Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: parent.label } StyledSwitch { id: toggle cLayer: 2 } } } ================================================ FILE: modules/controlcenter/bluetooth/DeviceList.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import Quickshell import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts DeviceList { id: root required property Session session readonly property bool smallDiscoverable: width <= 540 readonly property bool smallPairable: width <= 480 title: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length) description: qsTr("All available bluetooth devices") activeItem: session.bt.active model: ScriptModel { id: deviceModel values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)) } headerComponent: Component { RowLayout { spacing: Appearance.spacing.smaller StyledText { text: qsTr("Bluetooth") font.pointSize: Appearance.font.size.large font.weight: 500 } Item { Layout.fillWidth: true } ToggleButton { toggled: Bluetooth.defaultAdapter?.enabled ?? false icon: "power" accent: "Tertiary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Toggle Bluetooth") onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = !adapter.enabled; } } ToggleButton { toggled: Bluetooth.defaultAdapter?.discoverable ?? false icon: root.smallDiscoverable ? "group_search" : "" label: root.smallDiscoverable ? "" : qsTr("Discoverable") iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Make discoverable") onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discoverable = !adapter.discoverable; } } ToggleButton { toggled: Bluetooth.defaultAdapter?.pairable ?? false icon: "missing_controller" label: root.smallPairable ? "" : qsTr("Pairable") iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Make pairable") onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.pairable = !adapter.pairable; } } ToggleButton { toggled: Bluetooth.defaultAdapter?.discovering ?? false icon: "bluetooth_searching" accent: "Secondary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Scan for devices") onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discovering = !adapter.discovering; } } ToggleButton { toggled: !root.session.bt.active icon: "settings" accent: "Primary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Bluetooth settings") onClicked: { if (root.session.bt.active) root.session.bt.active = null; else { root.session.bt.active = root.model.values[0] ?? null; } } } } } delegate: Component { StyledRect { id: device required property BluetoothDevice modelData readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting) readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected width: ListView.view ? ListView.view.width : undefined implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { id: stateLayer function onClicked(): void { if (device.modelData) root.session.bt.active = device.modelData; } } RowLayout { id: deviceInner anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledRect { implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { anchors.fill: parent radius: parent.radius 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) } MaterialIcon { id: icon anchors.centerIn: parent text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.large fill: device.connected ? 1 : 0 Behavior on fill { Anim {} } } } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { Layout.fillWidth: true text: device.modelData ? device.modelData.name : qsTr("Unknown") elide: Text.ElideRight } StyledText { Layout.fillWidth: true text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small elide: Text.ElideRight } } StyledRect { id: connectBtn implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) CircularIndicator { anchors.fill: parent running: device.loading } StateLayer { function onClicked(): void { if (device.loading) return; if (device.connected) { device.modelData.connected = false; } else { if (device.modelData.bonded) { device.modelData.connected = true; } else { device.modelData.pair(); } } } color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface disabled: device.loading } MaterialIcon { id: connectIcon anchors.centerIn: parent animate: true text: device.connected ? "link_off" : "link" color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface opacity: device.loading ? 0 : 1 Behavior on opacity { Anim {} } } } } } } onItemSelected: item => session.bt.active = item } ================================================ FILE: modules/controlcenter/bluetooth/Settings.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property Session session spacing: Appearance.spacing.normal SettingsHeader { icon: "bluetooth" title: qsTr("Bluetooth Settings") } StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Adapter status") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("General adapter settings") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: adapterStatus anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.larger Toggle { label: qsTr("Powered") checked: Bluetooth.defaultAdapter?.enabled ?? false toggle.onToggled: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = checked; } } Toggle { label: qsTr("Discoverable") checked: Bluetooth.defaultAdapter?.discoverable ?? false toggle.onToggled: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discoverable = checked; } } Toggle { label: qsTr("Pairable") checked: Bluetooth.defaultAdapter?.pairable ?? false toggle.onToggled: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.pairable = checked; } } } } StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Adapter properties") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("Per-adapter settings") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: adapterSettings anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.larger RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Current adapter") } Item { id: adapterPickerButton property bool expanded implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2 implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 StateLayer { function onClicked(): void { adapterPickerButton.expanded = !adapterPickerButton.expanded; } radius: Appearance.rounding.small } RowLayout { id: adapterPicker anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.topMargin: Appearance.padding.smaller anchors.bottomMargin: Appearance.padding.smaller spacing: Appearance.spacing.normal StyledText { Layout.leftMargin: Appearance.padding.small text: Bluetooth.defaultAdapter?.name ?? qsTr("None") } MaterialIcon { text: "expand_more" } } Elevation { anchors.fill: adapterListBg radius: adapterListBg.radius opacity: adapterPickerButton.expanded ? 1 : 0 scale: adapterPickerButton.expanded ? 1 : 0.7 level: 2 Behavior on opacity { Anim {} } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } StyledClippingRect { id: adapterListBg anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight color: Colours.palette.m3secondaryContainer radius: Appearance.rounding.small opacity: adapterPickerButton.expanded ? 1 : 0 scale: adapterPickerButton.expanded ? 1 : 0.7 ColumnLayout { id: adapterList anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 0 Repeater { model: Bluetooth.adapters Item { id: adapter required property BluetoothAdapter modelData Layout.fillWidth: true implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { adapterPickerButton.expanded = false; root.session.bt.currentAdapter = adapter.modelData; } disabled: !adapterPickerButton.expanded } RowLayout { id: adapterInner anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true Layout.leftMargin: Appearance.padding.small text: adapter.modelData.name color: Colours.palette.m3onSecondaryContainer } MaterialIcon { text: "check" color: Colours.palette.m3onSecondaryContainer visible: adapter.modelData === root.session.bt.currentAdapter } } } } } Behavior on opacity { Anim {} } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Discoverable timeout") } CustomSpinBox { min: 0 value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0 onValueModified: value => { if (root.session.bt.currentAdapter) { root.session.bt.currentAdapter.discoverableTimeout = value; } } } } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.small Item { id: renameAdapter Layout.fillWidth: true Layout.rightMargin: Appearance.spacing.small implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight states: State { name: "editingAdapterName" when: root.session.bt.editingAdapterName AnchorChanges { target: adapterNameEdit anchors.top: renameAdapter.top } PropertyChanges { renameAdapter.implicitHeight: adapterNameEdit.implicitHeight renameLabel.opacity: 0 adapterNameEdit.padding: Appearance.padding.normal } } transitions: Transition { AnchorAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } Anim { properties: "implicitHeight,opacity,padding" } } StyledText { id: renameLabel anchors.left: parent.left text: qsTr("Rename adapter (currently does not work)") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledTextField { id: adapterNameEdit anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal text: root.session.bt.currentAdapter?.name ?? "" readOnly: !root.session.bt.editingAdapterName onAccepted: { root.session.bt.editingAdapterName = false; } leftPadding: Appearance.padding.normal rightPadding: Appearance.padding.normal background: StyledRect { radius: Appearance.rounding.small border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingAdapterName ? 1 : 0 Behavior on border.color { CAnim {} } Behavior on opacity { Anim {} } } Behavior on anchors.leftMargin { Anim {} } } } StyledRect { implicitWidth: implicitHeight implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.small color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingAdapterName ? 1 : 0 scale: root.session.bt.editingAdapterName ? 1 : 0.5 StateLayer { function onClicked(): void { root.session.bt.editingAdapterName = false; adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } color: Colours.palette.m3onSecondaryContainer disabled: !root.session.bt.editingAdapterName } MaterialIcon { id: cancelEditIcon anchors.centerIn: parent animate: true text: "cancel" color: Colours.palette.m3onSecondaryContainer } Behavior on opacity { Anim {} } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } StyledRect { implicitWidth: implicitHeight implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { function onClicked(): void { root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; if (root.session.bt.editingAdapterName) adapterNameEdit.forceActiveFocus(); else adapterNameEdit.accepted(); } color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { id: editIcon anchors.centerIn: parent animate: true text: root.session.bt.editingAdapterName ? "check_circle" : "edit" color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } Behavior on radius { Anim {} } } } } } StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Adapter information") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("Information about the default adapter") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: adapterInfo anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.small / 2 StyledText { text: qsTr("Adapter state") } StyledText { text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Dbus path") } StyledText { text: Bluetooth.defaultAdapter?.dbusPath ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Adapter id") } StyledText { text: Bluetooth.defaultAdapter?.adapterId ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } } } component Toggle: RowLayout { required property string label property alias checked: toggle.checked property alias toggle: toggle Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: parent.label } StyledSwitch { id: toggle cLayer: 2 } } } ================================================ FILE: modules/controlcenter/components/ConnectedButtonGroup.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root property var options: [] // Array of {label: string, propertyName: string, onToggled: function, state: bool?} property var rootItem: null // The root item that contains the properties we want to bind to property string title: "" // Optional title text property int rows: 1 // Number of rows Layout.fillWidth: true implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: true Behavior on implicitHeight { Anim {} } ColumnLayout { id: layout anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { visible: root.title !== "" text: root.title font.pointSize: Appearance.font.size.normal } GridLayout { id: buttonGrid Layout.alignment: Qt.AlignHCenter rowSpacing: Appearance.spacing.small columnSpacing: Appearance.spacing.small rows: root.rows columns: Math.ceil(root.options.length / root.rows) Repeater { id: repeater model: root.options delegate: TextButton { id: button required property int index required property var modelData property bool _checked: false Layout.fillWidth: true text: modelData.label checked: _checked toggle: false type: TextButton.Tonal // Create binding in Component.onCompleted Component.onCompleted: { if (button.modelData.state !== undefined && button.modelData.state) { _checked = button.modelData.state; } else if (root.rootItem && button.modelData.propertyName) { const propName = button.modelData.propertyName; const rootItem = root.rootItem; _checked = Qt.binding(function () { return rootItem[propName] ?? false; }); } } // Match utilities Toggles radius styling // Each button has full rounding (not connected) since they have spacing radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal // Match utilities Toggles inactive color inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) // Adjust width similar to utilities toggles Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) onClicked: { if (button.modelData.onToggled && root.rootItem && button.modelData.propertyName) { const currentValue = root.rootItem[button.modelData.propertyName] ?? false; button.modelData.onToggled(!currentValue); } } Behavior on Layout.preferredWidth { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Behavior on radius { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } } } } } ================================================ FILE: modules/controlcenter/components/DeviceDetails.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.config import QtQuick import QtQuick.Layouts Item { id: root property Session session property var device: null property Component headerComponent: null property list sections: [] property Component topContent: null property Component bottomContent: null implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight ColumnLayout { id: layout anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: Appearance.spacing.normal Loader { id: headerLoader Layout.fillWidth: true asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null } Loader { id: topContentLoader Layout.fillWidth: true asynchronous: true sourceComponent: root.topContent visible: root.topContent !== null } Repeater { model: root.sections Loader { required property Component modelData Layout.fillWidth: true asynchronous: true sourceComponent: modelData } } Loader { id: bottomContentLoader Layout.fillWidth: true asynchronous: true sourceComponent: root.bottomContent visible: root.bottomContent !== null } } } ================================================ FILE: modules/controlcenter/components/DeviceList.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.containers import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root property Session session: null property var model: null property Component delegate: null property string title: "" property string description: "" property var activeItem: null property Component headerComponent: null property Component titleSuffix: null property bool showHeader: true signal itemSelected(var item) spacing: Appearance.spacing.small Loader { id: headerLoader Layout.fillWidth: true asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null && root.showHeader } RowLayout { Layout.fillWidth: true Layout.topMargin: root.headerComponent ? 0 : 0 spacing: Appearance.spacing.small visible: root.title !== "" || root.description !== "" StyledText { visible: root.title !== "" text: root.title font.pointSize: Appearance.font.size.large font.weight: 500 } Loader { asynchronous: true sourceComponent: root.titleSuffix visible: root.titleSuffix !== null } Item { Layout.fillWidth: true } } property alias view: view StyledText { visible: root.description !== "" Layout.fillWidth: true text: root.description color: Colours.palette.m3outline } StyledListView { id: view Layout.fillWidth: true implicitHeight: contentHeight model: root.model delegate: root.delegate spacing: Appearance.spacing.small / 2 interactive: false clip: false } } ================================================ FILE: modules/controlcenter/components/PaneTransition.qml ================================================ pragma ComponentBehavior: Bound import qs.config import QtQuick SequentialAnimation { id: root required property Item target property list propertyActions property real scaleFrom: 1.0 property real scaleTo: 0.8 property real opacityFrom: 1.0 property real opacityTo: 0.0 ParallelAnimation { NumberAnimation { target: root.target property: "opacity" from: root.opacityFrom to: root.opacityTo duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standardAccel } NumberAnimation { target: root.target property: "scale" from: root.scaleFrom to: root.scaleTo duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standardAccel } } ScriptAction { script: { for (let i = 0; i < root.propertyActions.length; i++) { const action = root.propertyActions[i]; if (action.target && action.property !== undefined) { action.target[action.property] = action.value; } } } } ParallelAnimation { NumberAnimation { target: root.target property: "opacity" from: root.opacityTo to: root.opacityFrom duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standardDecel } NumberAnimation { target: root.target property: "scale" from: root.scaleTo to: root.scaleFrom duration: Appearance.anim.durations.normal / 2 easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standardDecel } } } ================================================ FILE: modules/controlcenter/components/ReadonlySlider.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root property string label: "" property real value: 0 property real from: 0 property real to: 100 property string suffix: "" property bool readonly: false spacing: Appearance.spacing.small RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { visible: root.label !== "" text: root.label font.pointSize: Appearance.font.size.normal color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface } Item { Layout.fillWidth: true } MaterialIcon { visible: root.readonly text: "lock" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") font.pointSize: Appearance.font.size.normal color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface } } StyledRect { Layout.fillWidth: true implicitHeight: Appearance.padding.normal radius: Appearance.rounding.full color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) opacity: root.readonly ? 0.5 : 1.0 StyledRect { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: parent.width * ((root.value - root.from) / (root.to - root.from)) radius: parent.radius color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary } } } ================================================ FILE: modules/controlcenter/components/SettingsHeader.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import QtQuick import QtQuick.Layouts Item { id: root required property string icon required property string title Layout.fillWidth: true implicitHeight: column.implicitHeight ColumnLayout { id: column anchors.centerIn: parent spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: root.icon font.pointSize: Appearance.font.size.extraLarge * 3 font.bold: true } StyledText { Layout.alignment: Qt.AlignHCenter text: root.title font.pointSize: Appearance.font.size.large font.bold: true } } } ================================================ FILE: modules/controlcenter/components/SliderInput.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root property string label: "" property real value: 0 property real from: 0 property real to: 100 property real stepSize: 0 property var validator: null property string suffix: "" // Optional suffix text (e.g., "×", "px") property int decimals: 1 // Number of decimal places to show (default: 1) property var formatValueFunction: null // Optional custom format function property var parseValueFunction: null // Optional custom parse function property bool _initialized: false signal valueModified(real newValue) function formatValue(val: real): string { if (formatValueFunction) { return formatValueFunction(val); } // Default format function // Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property) if (validator && validator.bottom !== undefined && validator.decimals === undefined) { return Math.round(val).toString(); } // For DoubleValidator or no validator, use the decimals property return val.toFixed(root.decimals); } function parseValue(text: string): real { if (parseValueFunction) { return parseValueFunction(text); } // Default parse function if (validator && validator.bottom !== undefined) { // Check if it's an integer validator if (validator.top !== undefined && validator.top === Math.floor(validator.top)) { return parseInt(text); } } return parseFloat(text); } spacing: Appearance.spacing.small Component.onCompleted: { // Set initialized flag after a brief delay to allow component to fully load Qt.callLater(() => { _initialized = true; }); } // Update input field when value changes externally (slider is already bound) onValueChanged: { // Only update if component is initialized to avoid issues during creation if (root._initialized && !inputField.hasFocus) { inputField.text = root.formatValue(root.value); } } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledText { visible: root.label !== "" text: root.label font.pointSize: Appearance.font.size.normal } Item { Layout.fillWidth: true } StyledInputField { id: inputField Layout.preferredWidth: 70 validator: root.validator Component.onCompleted: { // Initialize text without triggering valueModified signal text = root.formatValue(root.value); } onTextEdited: text => { if (hasFocus) { const val = root.parseValue(text); if (!isNaN(val)) { // Validate against validator bounds if available let isValid = true; if (root.validator) { if (root.validator.bottom !== undefined && val < root.validator.bottom) { isValid = false; } if (root.validator.top !== undefined && val > root.validator.top) { isValid = false; } } if (isValid) { root.valueModified(val); } } } } onEditingFinished: { const val = root.parseValue(text); let isValid = true; if (root.validator) { if (root.validator.bottom !== undefined && val < root.validator.bottom) { isValid = false; } if (root.validator.top !== undefined && val > root.validator.top) { isValid = false; } } if (isNaN(val) || !isValid) { text = root.formatValue(root.value); } } } StyledText { visible: root.suffix !== "" text: root.suffix color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } } StyledSlider { id: slider Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 from: root.from to: root.to stepSize: root.stepSize // Use Binding to allow slider to move freely during dragging Binding { target: slider property: "value" value: root.value when: !slider.pressed } onValueChanged: { // Update input field text in real-time as slider moves during dragging // Always update when slider value changes (during dragging or external updates) if (!inputField.hasFocus) { const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; inputField.text = root.formatValue(newValue); } } onMoved: { const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value; root.valueModified(newValue); if (!inputField.hasFocus) { inputField.text = root.formatValue(newValue); } } } } ================================================ FILE: modules/controlcenter/components/SplitPaneLayout.qml ================================================ pragma ComponentBehavior: Bound import qs.components.effects import qs.config import Quickshell.Widgets import QtQuick import QtQuick.Layouts RowLayout { id: root property Component leftContent: null property Component rightContent: null property real leftWidthRatio: 0.4 property int leftMinimumWidth: 420 property var leftLoaderProperties: ({}) property var rightLoaderProperties: ({}) property alias leftLoader: leftLoader property alias rightLoader: rightLoader spacing: 0 Item { id: leftPane Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio) Layout.minimumWidth: root.leftMinimumWidth Layout.fillHeight: true ClippingRectangle { id: leftClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 anchors.rightMargin: Appearance.padding.normal / 2 radius: leftBorder.innerRadius color: "transparent" Loader { id: leftLoader anchors.fill: parent anchors.margins: Appearance.padding.large + Appearance.padding.normal anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 asynchronous: true sourceComponent: root.leftContent Component.onCompleted: { for (const key in root.leftLoaderProperties) { leftLoader[key] = root.leftLoaderProperties[key]; } } } } InnerBorder { id: leftBorder leftThickness: 0 rightThickness: Appearance.padding.normal / 2 } } Item { id: rightPane Layout.fillWidth: true Layout.fillHeight: true ClippingRectangle { id: rightClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 anchors.rightMargin: Appearance.padding.normal / 2 radius: rightBorder.innerRadius color: "transparent" Loader { id: rightLoader anchors.fill: parent anchors.margins: Appearance.padding.large * 2 asynchronous: true sourceComponent: root.rightContent Component.onCompleted: { for (const key in root.rightLoaderProperties) { rightLoader[key] = root.rightLoaderProperties[key]; } } } } InnerBorder { id: rightBorder leftThickness: Appearance.padding.normal / 2 } } } ================================================ FILE: modules/controlcenter/components/SplitPaneWithDetails.qml ================================================ pragma ComponentBehavior: Bound import QtQuick Item { id: root required property Component leftContent required property Component rightDetailsComponent required property Component rightSettingsComponent property var activeItem: null property var paneIdGenerator: function (item) { return item ? String(item) : ""; } property Component overlayComponent: null SplitPaneLayout { id: splitLayout anchors.fill: parent leftContent: root.leftContent rightContent: Component { Item { id: rightPaneItem property var pane: root.activeItem property string paneId: root.paneIdGenerator(pane) property Component targetComponent: root.rightSettingsComponent property Component nextComponent: root.rightSettingsComponent function getComponentForPane() { return pane ? root.rightDetailsComponent : root.rightSettingsComponent; } Component.onCompleted: { targetComponent = getComponentForPane(); nextComponent = targetComponent; } Loader { id: rightLoader anchors.fill: parent asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center clip: false sourceComponent: rightPaneItem.targetComponent } Behavior on paneId { PaneTransition { target: rightLoader propertyActions: [ PropertyAction { target: rightPaneItem property: "targetComponent" value: rightPaneItem.nextComponent } ] } } onPaneChanged: { nextComponent = getComponentForPane(); paneId = root.paneIdGenerator(pane); } } } } Loader { id: overlayLoader anchors.fill: parent asynchronous: true z: 1000 sourceComponent: root.overlayComponent active: root.overlayComponent !== null } } ================================================ FILE: modules/controlcenter/components/WallpaperGrid.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.controls import qs.components.images import qs.services import qs.config import QtQuick GridView { id: root required property Session session readonly property int minCellWidth: 200 + Appearance.spacing.normal readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) cellWidth: width / columnsCount cellHeight: 140 + Appearance.spacing.normal model: Wallpapers.list clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: root } delegate: Item { id: wpDelegate required property var modelData required property int index readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent readonly property real itemMargin: Appearance.spacing.normal / 2 readonly property real itemRadius: Appearance.rounding.normal width: root.cellWidth height: root.cellHeight StateLayer { function onClicked(): void { Wallpapers.setWallpaper(wpDelegate.modelData.path); } anchors.fill: parent anchors.leftMargin: wpDelegate.itemMargin anchors.rightMargin: wpDelegate.itemMargin anchors.topMargin: wpDelegate.itemMargin anchors.bottomMargin: wpDelegate.itemMargin radius: wpDelegate.itemRadius } StyledClippingRect { id: image anchors.fill: parent anchors.leftMargin: wpDelegate.itemMargin anchors.rightMargin: wpDelegate.itemMargin anchors.topMargin: wpDelegate.itemMargin anchors.bottomMargin: wpDelegate.itemMargin color: Colours.tPalette.m3surfaceContainer radius: wpDelegate.itemRadius antialiasing: true layer.enabled: true layer.smooth: true CachingImage { id: cachingImage path: wpDelegate.modelData.path anchors.fill: parent fillMode: Image.PreserveAspectCrop cache: true visible: opacity > 0 antialiasing: true smooth: true sourceSize: Qt.size(width, height) opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutQuad } } } // Fallback if CachingImage fails to load Image { id: fallbackImage anchors.fill: parent source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? wpDelegate.modelData.path : "" asynchronous: true fillMode: Image.PreserveAspectCrop cache: true visible: opacity > 0 antialiasing: true smooth: true sourceSize: Qt.size(width, height) opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutQuad } } } Timer { id: fallbackTimer property bool triggered: false interval: 800 running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null onTriggered: triggered = true } // Gradient overlay for filename Rectangle { id: filenameOverlay anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 radius: 0 gradient: Gradient { GradientStop { position: 0.0 color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0) } GradientStop { position: 0.3 color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.7) } GradientStop { position: 0.6 color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.9) } GradientStop { position: 1.0 color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.95) } } opacity: 0 Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } Component.onCompleted: { opacity = 1; } } } Rectangle { anchors.fill: parent anchors.leftMargin: wpDelegate.itemMargin anchors.rightMargin: wpDelegate.itemMargin anchors.topMargin: wpDelegate.itemMargin anchors.bottomMargin: wpDelegate.itemMargin color: "transparent" radius: wpDelegate.itemRadius + border.width border.width: wpDelegate.isCurrent ? 2 : 0 border.color: Colours.palette.m3primary antialiasing: true smooth: true Behavior on border.width { NumberAnimation { duration: 150 easing.type: Easing.OutQuad } } MaterialIcon { anchors.right: parent.right anchors.top: parent.top anchors.margins: Appearance.padding.small visible: wpDelegate.isCurrent text: "check_circle" color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large } } StyledText { id: filenameText anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 anchors.bottomMargin: Appearance.padding.normal text: wpDelegate.modelData.name font.pointSize: Appearance.font.size.smaller font.weight: 500 color: wpDelegate.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface elide: Text.ElideMiddle maximumLineCount: 1 horizontalAlignment: Text.AlignHCenter opacity: 0 Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } Component.onCompleted: { opacity = 1; } } } } ================================================ FILE: modules/controlcenter/dashboard/DashboardPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.controls import qs.components.effects import qs.components.containers import qs.config import Quickshell.Widgets import QtQuick import QtQuick.Layouts Item { id: root required property Session session // General Settings property bool enabled: Config.dashboard.enabled ?? true property bool showOnHover: Config.dashboard.showOnHover ?? true property int mediaUpdateInterval: Config.dashboard.mediaUpdateInterval ?? 1000 property int resourceUpdateInterval: Config.dashboard.resourceUpdateInterval ?? 1000 property int dragThreshold: Config.dashboard.dragThreshold ?? 50 // Dashboard Tabs property bool showDashboard: Config.dashboard.showDashboard ?? true property bool showMedia: Config.dashboard.showMedia ?? true property bool showPerformance: Config.dashboard.showPerformance ?? true property bool showWeather: Config.dashboard.showWeather ?? true // Performance Resources property bool showBattery: Config.dashboard.performance.showBattery ?? false property bool showGpu: Config.dashboard.performance.showGpu ?? true property bool showCpu: Config.dashboard.performance.showCpu ?? true property bool showMemory: Config.dashboard.performance.showMemory ?? true property bool showStorage: Config.dashboard.performance.showStorage ?? true property bool showNetwork: Config.dashboard.performance.showNetwork ?? true function saveConfig() { Config.dashboard.enabled = root.enabled; Config.dashboard.showOnHover = root.showOnHover; Config.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; Config.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; Config.dashboard.dragThreshold = root.dragThreshold; Config.dashboard.showDashboard = root.showDashboard; Config.dashboard.showMedia = root.showMedia; Config.dashboard.showPerformance = root.showPerformance; Config.dashboard.showWeather = root.showWeather; Config.dashboard.performance.showBattery = root.showBattery; Config.dashboard.performance.showGpu = root.showGpu; Config.dashboard.performance.showCpu = root.showCpu; Config.dashboard.performance.showMemory = root.showMemory; Config.dashboard.performance.showStorage = root.showStorage; Config.dashboard.performance.showNetwork = root.showNetwork; // Note: sizes properties are readonly and cannot be modified Config.save(); } anchors.fill: parent ClippingRectangle { id: dashboardClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 anchors.rightMargin: Appearance.padding.normal radius: dashboardBorder.innerRadius color: "transparent" Loader { id: dashboardLoader anchors.fill: parent anchors.margins: Appearance.padding.large + Appearance.padding.normal anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large asynchronous: true sourceComponent: dashboardContentComponent } } InnerBorder { id: dashboardBorder leftThickness: 0 rightThickness: Appearance.padding.normal } Component { id: dashboardContentComponent StyledFlickable { id: dashboardFlickable flickableDirection: Flickable.VerticalFlick contentHeight: dashboardLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: dashboardFlickable } ColumnLayout { id: dashboardLayout anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: Appearance.spacing.normal RowLayout { spacing: Appearance.spacing.smaller StyledText { text: qsTr("Dashboard") font.pointSize: Appearance.font.size.large font.weight: 500 } } // General Settings Section GeneralSection { rootItem: root } // Performance Resources Section PerformanceSection { rootItem: root } } } } } ================================================ FILE: modules/controlcenter/dashboard/GeneralSection.qml ================================================ import "../components" import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts SectionContainer { id: root required property var rootItem Layout.fillWidth: true alignTop: true StyledText { text: qsTr("General Settings") font.pointSize: Appearance.font.size.normal } SwitchRow { label: qsTr("Enabled") checked: root.rootItem.enabled onToggled: checked => { root.rootItem.enabled = checked; root.rootItem.saveConfig(); } } SwitchRow { label: qsTr("Show on hover") checked: root.rootItem.showOnHover onToggled: checked => { root.rootItem.showOnHover = checked; root.rootItem.saveConfig(); } } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal SwitchRow { Layout.fillWidth: true label: qsTr("Show Dashboard tab") checked: root.rootItem.showDashboard onToggled: checked => { root.rootItem.showDashboard = checked; root.rootItem.saveConfig(); } } SwitchRow { Layout.fillWidth: true label: qsTr("Show Media tab") checked: root.rootItem.showMedia onToggled: checked => { root.rootItem.showMedia = checked; root.rootItem.saveConfig(); } } SwitchRow { Layout.fillWidth: true label: qsTr("Show Performance tab") checked: root.rootItem.showPerformance onToggled: checked => { root.rootItem.showPerformance = checked; root.rootItem.saveConfig(); } } SwitchRow { Layout.fillWidth: true label: qsTr("Show Weather tab") checked: root.rootItem.showWeather onToggled: checked => { root.rootItem.showWeather = checked; root.rootItem.saveConfig(); } } } SliderInput { Layout.fillWidth: true label: qsTr("Media update interval") value: root.rootItem.mediaUpdateInterval from: 100 to: 10000 stepSize: 100 suffix: "ms" validator: IntValidator { bottom: 100 top: 10000 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootItem.mediaUpdateInterval = Math.round(newValue); root.rootItem.saveConfig(); } } SliderInput { Layout.fillWidth: true label: qsTr("Drag threshold") value: root.rootItem.dragThreshold from: 0 to: 100 suffix: "px" validator: IntValidator { bottom: 0 top: 100 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootItem.dragThreshold = Math.round(newValue); root.rootItem.saveConfig(); } } } ================================================ FILE: modules/controlcenter/dashboard/PerformanceSection.qml ================================================ import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower import qs.components import qs.config import qs.services SectionContainer { id: root required property var rootItem // GPU toggle is hidden when gpuType is "NONE" (no GPU data available) readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" // Battery toggle is hidden when no laptop battery is present readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Performance Resources") font.pointSize: Appearance.font.size.normal } ConnectedButtonGroup { rootItem: root.rootItem options: { let opts = []; if (root.batteryAvailable) opts.push({ "label": qsTr("Battery"), "propertyName": "showBattery", "onToggled": function (checked) { root.rootItem.showBattery = checked; root.rootItem.saveConfig(); } }); if (root.gpuAvailable) opts.push({ "label": qsTr("GPU"), "propertyName": "showGpu", "onToggled": function (checked) { root.rootItem.showGpu = checked; root.rootItem.saveConfig(); } }); opts.push({ "label": qsTr("CPU"), "propertyName": "showCpu", "onToggled": function (checked) { root.rootItem.showCpu = checked; root.rootItem.saveConfig(); } }, { "label": qsTr("Memory"), "propertyName": "showMemory", "onToggled": function (checked) { root.rootItem.showMemory = checked; root.rootItem.saveConfig(); } }, { "label": qsTr("Storage"), "propertyName": "showStorage", "onToggled": function (checked) { root.rootItem.showStorage = checked; root.rootItem.saveConfig(); } }, { "label": qsTr("Network"), "propertyName": "showNetwork", "onToggled": function (checked) { root.rootItem.showNetwork = checked; root.rootItem.saveConfig(); } }); return opts; } } SliderInput { Layout.fillWidth: true label: qsTr("Resource update interval") value: root.rootItem.resourceUpdateInterval from: 100 to: 10000 stepSize: 100 suffix: "ms" validator: IntValidator { bottom: 100 top: 10000 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.rootItem.resourceUpdateInterval = Math.round(newValue); root.rootItem.saveConfig(); } } } ================================================ FILE: modules/controlcenter/launcher/LauncherPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.components.containers import qs.services import qs.config import qs.utils import Caelestia import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts import "../../../utils/scripts/fuzzysort.js" as Fuzzy Item { id: root required property Session session property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false property bool favouriteChecked: false property string searchText: "" property list filteredApps: [] function updateToggleState() { if (!root.selectedApp) { root.hideFromLauncherChecked = false; root.favouriteChecked = false; return; } const appId = root.selectedApp.id || root.selectedApp.entry?.id; root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); } function saveHiddenApps(isHidden) { if (!root.selectedApp) { return; } const appId = root.selectedApp.id || root.selectedApp.entry?.id; const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; if (isHidden) { if (!hiddenApps.includes(appId)) { hiddenApps.push(appId); } } else { const index = hiddenApps.indexOf(appId); if (index !== -1) { hiddenApps.splice(index, 1); } } Config.launcher.hiddenApps = hiddenApps; Config.save(); } function filterApps(search: string): list { if (!search || search.trim() === "") { const apps = []; for (let i = 0; i < allAppsDb.apps.length; i++) { apps.push(allAppsDb.apps[i]); } return apps; } if (!allAppsDb.apps || allAppsDb.apps.length === 0) { return []; } const preparedApps = []; for (let i = 0; i < allAppsDb.apps.length; i++) { const app = allAppsDb.apps[i]; const name = app.name || app.entry?.name || ""; preparedApps.push({ _item: app, name: Fuzzy.prepare(name) }); } const results = Fuzzy.go(search, preparedApps, { all: true, keys: ["name"], scoreFn: r => r[0].score }); return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); } function updateFilteredApps() { filteredApps = filterApps(searchText); } anchors.fill: parent onSelectedAppChanged: { session.launcher.active = selectedApp; updateToggleState(); } onSearchTextChanged: { updateFilteredApps(); } Component.onCompleted: { updateFilteredApps(); } Connections { function onActiveChanged() { root.selectedApp = root.session.launcher.active; root.updateToggleState(); } target: root.session.launcher } AppDb { id: allAppsDb path: `${Paths.state}/apps.sqlite` favouriteApps: Config.launcher.favouriteApps entries: DesktopEntries.applications.values } Connections { function onAppsChanged() { root.updateFilteredApps(); } target: allAppsDb } SplitPaneLayout { anchors.fill: parent leftContent: Component { ColumnLayout { id: leftLauncherLayout anchors.fill: parent spacing: Appearance.spacing.small RowLayout { spacing: Appearance.spacing.smaller StyledText { text: qsTr("Launcher") font.pointSize: Appearance.font.size.large font.weight: 500 } Item { Layout.fillWidth: true } ToggleButton { toggled: !root.session.launcher.active icon: "settings" accent: "Primary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Launcher settings") onClicked: { if (root.session.launcher.active) { root.session.launcher.active = null; } else { if (root.filteredApps.length > 0) { root.session.launcher.active = root.filteredApps[0]; } } } } } StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) font.pointSize: Appearance.font.size.normal font.weight: 500 } StyledText { text: qsTr("All applications available in the launcher") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal Layout.bottomMargin: Appearance.spacing.small color: Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.full implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) MaterialIcon { id: searchIcon anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: Appearance.padding.normal text: "search" color: Colours.palette.m3onSurfaceVariant } StyledTextField { id: searchField anchors.left: searchIcon.right anchors.right: clearIcon.left anchors.leftMargin: Appearance.spacing.small anchors.rightMargin: Appearance.spacing.small topPadding: Appearance.padding.normal bottomPadding: Appearance.padding.normal placeholderText: qsTr("Search applications...") onTextChanged: { root.searchText = text; } } MaterialIcon { id: clearIcon anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: Appearance.padding.normal width: searchField.text ? implicitWidth : implicitWidth / 2 opacity: { if (!searchField.text) return 0; if (clearMouse.pressed) return 0.7; if (clearMouse.containsMouse) return 0.8; return 1; } text: "close" color: Colours.palette.m3onSurfaceVariant MouseArea { id: clearMouse anchors.fill: parent hoverEnabled: true cursorShape: searchField.text ? Qt.PointingHandCursor : undefined onClicked: searchField.text = "" } Behavior on width { Anim { duration: Appearance.anim.durations.small } } Behavior on opacity { Anim { duration: Appearance.anim.durations.small } } } } Loader { id: appsListLoader Layout.fillWidth: true Layout.fillHeight: true asynchronous: true active: true sourceComponent: StyledListView { id: appsListView Layout.fillWidth: true Layout.fillHeight: true model: root.filteredApps spacing: Appearance.spacing.small / 2 clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: appsListView } delegate: StyledRect { id: appDelegate required property var modelData readonly property bool isSelected: root.selectedApp === modelData width: parent ? parent.width : 0 implicitHeight: 40 color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal opacity: 0 Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } Component.onCompleted: { opacity = 1; } StateLayer { function onClicked(): void { root.session.launcher.active = appDelegate.modelData; } } RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal IconImage { asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: 32 source: { const entry = appDelegate.modelData.entry; return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; } } StyledText { Layout.fillWidth: true text: appDelegate.modelData.name || appDelegate.modelData.entry?.name || qsTr("Unknown") font.pointSize: Appearance.font.size.normal } Loader { readonly property bool isHidden: appDelegate.modelData ? Strings.testRegexList(Config.launcher.hiddenApps, appDelegate.modelData.id) : false readonly property bool isFav: appDelegate.modelData ? Strings.testRegexList(Config.launcher.favouriteApps, appDelegate.modelData.id) : false Layout.alignment: Qt.AlignVCenter asynchronous: true active: isHidden || isFav sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) } Component { id: hiddenIcon MaterialIcon { text: "visibility_off" fill: 1 color: Colours.palette.m3primary } } Component { id: favouriteIcon MaterialIcon { text: "favorite" fill: 1 color: Colours.palette.m3primary } } } } } } } } rightContent: Component { Item { id: rightLauncherPane property var pane: root.session.launcher.active property string paneId: pane ? (pane.id || pane.entry?.id || "") : "" property Component targetComponent: settings property Component nextComponent: settings property var displayedApp: null function getComponentForPane() { return pane ? appDetails : settings; } Component.onCompleted: { displayedApp = pane; targetComponent = getComponentForPane(); nextComponent = targetComponent; } onPaneChanged: { nextComponent = getComponentForPane(); paneId = pane ? (pane.id || pane.entry?.id || "") : ""; } onDisplayedAppChanged: { if (displayedApp) { const appId = displayedApp.id || displayedApp.entry?.id; root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); } else { root.hideFromLauncherChecked = false; root.favouriteChecked = false; } } Loader { id: rightLauncherLoader property var displayedApp: rightLauncherPane.displayedApp anchors.fill: parent asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center clip: false sourceComponent: rightLauncherPane.targetComponent active: true onItemChanged: { if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { rightLauncherPane.displayedApp = rightLauncherPane.pane; } } } Behavior on paneId { PaneTransition { target: rightLauncherLoader propertyActions: [ PropertyAction { target: rightLauncherPane property: "displayedApp" value: rightLauncherPane.pane }, PropertyAction { target: rightLauncherLoader property: "active" value: false }, PropertyAction { target: rightLauncherPane property: "targetComponent" value: rightLauncherPane.nextComponent }, PropertyAction { target: rightLauncherLoader property: "active" value: true } ] } } } } } Component { id: settings StyledFlickable { id: settingsFlickable flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height StyledScrollBar.vertical: StyledScrollBar { flickable: settingsFlickable } Settings { id: settingsInner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session } } } Component { id: appDetails ColumnLayout { id: appDetailsLayout readonly property var displayedApp: parent?.displayedApp ?? null // qmllint disable missing-property anchors.fill: parent spacing: Appearance.spacing.normal SettingsHeader { Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.topMargin: Appearance.padding.large * 2 visible: appDetailsLayout.displayedApp === null icon: "apps" title: qsTr("Launcher Applications") } Item { Layout.alignment: Qt.AlignHCenter Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.topMargin: Appearance.padding.large * 2 visible: appDetailsLayout.displayedApp !== null implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight ColumnLayout { anchors.centerIn: parent spacing: Appearance.spacing.normal IconImage { id: appIconImage asynchronous: true Layout.alignment: Qt.AlignHCenter implicitSize: Appearance.font.size.extraLarge * 3 * 2 source: { const app = appDetailsLayout.displayedApp; if (!app) return "image-missing"; const entry = app.entry; if (entry && entry.icon) { return Quickshell.iconPath(entry.icon, "image-missing"); } return "image-missing"; } } StyledText { id: appTitleText Layout.alignment: Qt.AlignHCenter text: appDetailsLayout.displayedApp.displayedApp ? (appDetailsLayout.displayedApp.displayedApp.displayedApp.name || appDetailsLayout.displayedApp.displayedApp.displayedApp.entry?.name || qsTr("Application Details")) : "" font.pointSize: Appearance.font.size.large font.bold: true } } } Item { Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: Appearance.spacing.large Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 StyledFlickable { id: detailsFlickable anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: debugLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: parent } ColumnLayout { id: debugLayout anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: Appearance.spacing.normal SwitchRow { Layout.topMargin: Appearance.spacing.normal visible: appDetailsLayout.displayedApp !== null label: qsTr("Mark as favourite") checked: root.favouriteChecked // disabled if: // * app is hidden // * app isn't in favouriteApps array but marked as favourite anyway // ^^^ This means that this app is favourited because of a regex check // this button can not toggle regexed apps enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) opacity: enabled ? 1 : 0.6 onToggled: checked => { root.favouriteChecked = checked; const app = appDetailsLayout.displayedApp; if (app) { const appId = app.id || app.entry?.id; const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : []; if (checked) { if (!favouriteApps.includes(appId)) { favouriteApps.push(appId); } } else { const index = favouriteApps.indexOf(appId); if (index !== -1) { favouriteApps.splice(index, 1); } } Config.launcher.favouriteApps = favouriteApps; Config.save(); } } } SwitchRow { Layout.topMargin: Appearance.spacing.normal visible: appDetailsLayout.displayedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked // disabled if: // * app is favourited // * app isn't in hiddenApps array but marked as hidden anyway // ^^^ This means that this app is hidden because of a regex check // this button can not toggle regexed apps enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) opacity: enabled ? 1 : 0.6 onToggled: checked => { root.hideFromLauncherChecked = checked; const app = appDetailsLayout.displayedApp; if (app) { const appId = app.id || app.entry?.id; const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; if (checked) { if (!hiddenApps.includes(appId)) { hiddenApps.push(appId); } } else { const index = hiddenApps.indexOf(appId); if (index !== -1) { hiddenApps.splice(index, 1); } } Config.launcher.hiddenApps = hiddenApps; Config.save(); } } } } } } } } } ================================================ FILE: modules/controlcenter/launcher/Settings.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property Session session spacing: Appearance.spacing.normal SettingsHeader { icon: "apps" title: qsTr("Launcher Settings") } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("General") description: qsTr("General launcher settings") } SectionContainer { ToggleRow { label: qsTr("Enabled") checked: Config.launcher.enabled toggle.onToggled: { Config.launcher.enabled = checked; Config.save(); } } ToggleRow { label: qsTr("Show on hover") checked: Config.launcher.showOnHover toggle.onToggled: { Config.launcher.showOnHover = checked; Config.save(); } } ToggleRow { label: qsTr("Vim keybinds") checked: Config.launcher.vimKeybinds toggle.onToggled: { Config.launcher.vimKeybinds = checked; Config.save(); } } ToggleRow { label: qsTr("Enable dangerous actions") checked: Config.launcher.enableDangerousActions toggle.onToggled: { Config.launcher.enableDangerousActions = checked; Config.save(); } } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Display") description: qsTr("Display and appearance settings") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Max shown items") value: qsTr("%1").arg(Config.launcher.maxShown) } PropertyRow { showTopMargin: true label: qsTr("Max wallpapers") value: qsTr("%1").arg(Config.launcher.maxWallpapers) } PropertyRow { showTopMargin: true label: qsTr("Drag threshold") value: qsTr("%1 px").arg(Config.launcher.dragThreshold) } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Prefixes") description: qsTr("Command prefix settings") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Special prefix") value: Config.launcher.specialPrefix || qsTr("None") } PropertyRow { showTopMargin: true label: qsTr("Action prefix") value: Config.launcher.actionPrefix || qsTr("None") } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Fuzzy search") description: qsTr("Fuzzy search settings") } SectionContainer { ToggleRow { label: qsTr("Apps") checked: Config.launcher.useFuzzy.apps toggle.onToggled: { Config.launcher.useFuzzy.apps = checked; Config.save(); } } ToggleRow { label: qsTr("Actions") checked: Config.launcher.useFuzzy.actions toggle.onToggled: { Config.launcher.useFuzzy.actions = checked; Config.save(); } } ToggleRow { label: qsTr("Schemes") checked: Config.launcher.useFuzzy.schemes toggle.onToggled: { Config.launcher.useFuzzy.schemes = checked; Config.save(); } } ToggleRow { label: qsTr("Variants") checked: Config.launcher.useFuzzy.variants toggle.onToggled: { Config.launcher.useFuzzy.variants = checked; Config.save(); } } ToggleRow { label: qsTr("Wallpapers") checked: Config.launcher.useFuzzy.wallpapers toggle.onToggled: { Config.launcher.useFuzzy.wallpapers = checked; Config.save(); } } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Sizes") description: qsTr("Size settings for launcher items") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Item width") value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth) } PropertyRow { showTopMargin: true label: qsTr("Item height") value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight) } PropertyRow { showTopMargin: true label: qsTr("Wallpaper width") value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth) } PropertyRow { showTopMargin: true label: qsTr("Wallpaper height") value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight) } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Hidden apps") description: qsTr("Applications hidden from launcher") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Total hidden") value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) } } } ================================================ FILE: modules/controlcenter/network/EthernetDetails.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts DeviceDetails { id: root required property Session session readonly property var ethernetDevice: root.session.ethernet.active device: ethernetDevice Component.onCompleted: { if (ethernetDevice && ethernetDevice.interface) { Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); } } onEthernetDeviceChanged: { if (ethernetDevice && ethernetDevice.interface) { Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {}); } else { Nmcli.ethernetDeviceDetails = null; } } headerComponent: Component { ConnectionHeader { icon: "cable" title: root.ethernetDevice?.interface ?? qsTr("Unknown") } } sections: [ Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Connection status") description: qsTr("Connection settings for this device") } SectionContainer { ToggleRow { label: qsTr("Connected") checked: root.ethernetDevice?.connected ?? false toggle.onToggled: { if (checked) { Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {}); } else { if (root.ethernetDevice?.connection) { Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {}); } } } } } } }, Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Device properties") description: qsTr("Additional information") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Interface") value: root.ethernetDevice?.interface ?? qsTr("Unknown") } PropertyRow { showTopMargin: true label: qsTr("Connection") value: root.ethernetDevice?.connection || qsTr("Not connected") } PropertyRow { showTopMargin: true label: qsTr("State") value: root.ethernetDevice?.state ?? qsTr("Unknown") } } } }, Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Connection information") description: qsTr("Network connection details") } SectionContainer { ConnectionInfoSection { deviceDetails: Nmcli.ethernetDeviceDetails } } } } ] } ================================================ FILE: modules/controlcenter/network/EthernetList.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts DeviceList { id: root required property Session session title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) description: qsTr("All available ethernet devices") activeItem: session.ethernet.active model: Nmcli.ethernetDevices headerComponent: Component { RowLayout { spacing: Appearance.spacing.smaller StyledText { text: qsTr("Settings") font.pointSize: Appearance.font.size.large font.weight: 500 } Item { Layout.fillWidth: true } ToggleButton { toggled: !root.session.ethernet.active icon: "settings" accent: "Primary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller onClicked: { if (root.session.ethernet.active) root.session.ethernet.active = null; else { root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null; } } } } } delegate: Component { StyledRect { id: ethernetItem required property var modelData readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface width: ListView.view ? ListView.view.width : undefined implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { id: stateLayer function onClicked(): void { root.session.ethernet.active = ethernetItem.modelData; } } RowLayout { id: rowLayout anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledRect { implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: ethernetItem.modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { anchors.fill: parent radius: parent.radius color: Qt.alpha(ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) } MaterialIcon { id: icon anchors.centerIn: parent text: "cable" font.pointSize: Appearance.font.size.large fill: ethernetItem.modelData.connected ? 1 : 0 color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface Behavior on fill { Anim {} } } } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { Layout.fillWidth: true text: ethernetItem.modelData.interface || qsTr("Unknown") elide: Text.ElideRight } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { Layout.fillWidth: true text: ethernetItem.modelData.connected ? qsTr("Connected") : qsTr("Disconnected") color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline font.pointSize: Appearance.font.size.small font.weight: ethernetItem.modelData.connected ? 500 : 400 elide: Text.ElideRight } } } StyledRect { id: connectBtn implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, ethernetItem.modelData.connected ? 1 : 0) StateLayer { function onClicked(): void { if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); } else { Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); } } color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } MaterialIcon { id: connectIcon anchors.centerIn: parent animate: true text: ethernetItem.modelData.connected ? "link_off" : "link" color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } } } } onItemSelected: function (item) { session.ethernet.active = item; } } ================================================ FILE: modules/controlcenter/network/EthernetPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components.containers import QtQuick SplitPaneWithDetails { id: root required property Session session anchors.fill: parent activeItem: session.ethernet.active paneIdGenerator: function (item) { return item ? (item.interface || "") : ""; } leftContent: Component { EthernetList { session: root.session } } rightDetailsComponent: Component { EthernetDetails { session: root.session } } rightSettingsComponent: Component { StyledFlickable { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height clip: true EthernetSettings { id: settingsInner anchors.left: parent.left anchors.right: parent.right session: root.session } } } } ================================================ FILE: modules/controlcenter/network/EthernetSettings.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property Session session spacing: Appearance.spacing.normal SettingsHeader { icon: "cable" title: qsTr("Ethernet settings") } StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Ethernet devices") font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { text: qsTr("Available ethernet devices") color: Colours.palette.m3outline } StyledRect { Layout.fillWidth: true implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: ethernetInfo anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.small / 2 StyledText { text: qsTr("Total devices") } StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Connected devices") } StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } } } } ================================================ FILE: modules/controlcenter/network/NetworkSettings.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.components.containers import qs.services import qs.config import QtQuick import QtQuick.Controls import QtQuick.Layouts ColumnLayout { id: root required property Session session spacing: Appearance.spacing.normal SettingsHeader { icon: "router" title: qsTr("Network Settings") } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Ethernet") description: qsTr("Ethernet device information") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Total devices") value: qsTr("%1").arg(Nmcli.ethernetDevices.length) } PropertyRow { showTopMargin: true label: qsTr("Connected devices") value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Wireless") description: qsTr("WiFi network settings") } SectionContainer { ToggleRow { label: qsTr("WiFi enabled") checked: Nmcli.wifiEnabled toggle.onToggled: { Nmcli.enableWifi(checked); } } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("VPN") description: qsTr("VPN provider settings") visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 } SectionContainer { visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 ToggleRow { label: qsTr("VPN enabled") checked: Config.utilities.vpn.enabled toggle.onToggled: { Config.utilities.vpn.enabled = checked; Config.save(); } } PropertyRow { showTopMargin: true label: qsTr("Providers") value: qsTr("%1").arg(Config.utilities.vpn.provider.length) } TextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 text: qsTr("⚙ Manage VPN Providers") inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer onClicked: { vpnSettingsDialog.open(); } } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Current connection") description: qsTr("Active network connection information") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Network") value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected")) } PropertyRow { showTopMargin: true visible: Nmcli.active !== null label: qsTr("Signal strength") value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") } PropertyRow { showTopMargin: true visible: Nmcli.active !== null label: qsTr("Security") value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") } PropertyRow { showTopMargin: true visible: Nmcli.active !== null label: qsTr("Frequency") value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") } } Popup { id: vpnSettingsDialog parent: Overlay.overlay anchors.centerIn: parent width: Math.min(600, parent.width - Appearance.padding.large * 2) height: Math.min(700, parent.height - Appearance.padding.large * 2) modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: StyledRect { color: Colours.palette.m3surface radius: Appearance.rounding.large } StyledFlickable { anchors.fill: parent anchors.margins: Appearance.padding.large * 1.5 flickableDirection: Flickable.VerticalFlick contentHeight: vpnSettingsContent.height clip: true VpnSettings { id: vpnSettingsContent anchors.left: parent.left anchors.right: parent.right session: root.session } } } } ================================================ FILE: modules/controlcenter/network/NetworkingPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import "." import qs.components import qs.components.controls import qs.components.containers import qs.services import qs.config import QtQuick import QtQuick.Layouts Item { id: root required property Session session anchors.fill: parent SplitPaneLayout { id: splitLayout anchors.fill: parent leftContent: Component { StyledFlickable { id: leftFlickable flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height StyledScrollBar.vertical: StyledScrollBar { flickable: leftFlickable } ColumnLayout { id: leftContent anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.normal RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { text: qsTr("Network") font.pointSize: Appearance.font.size.large font.weight: 500 } Item { Layout.fillWidth: true } ToggleButton { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Toggle WiFi") onClicked: { Nmcli.toggleWifi(null); } } ToggleButton { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Scan for networks") onClicked: { Nmcli.rescanWifi(); } } ToggleButton { toggled: !root.session.ethernet.active && !root.session.network.active icon: "settings" accent: "Primary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller tooltip: qsTr("Network settings") onClicked: { if (root.session.ethernet.active || root.session.network.active) { root.session.ethernet.active = null; root.session.network.active = null; } else { if (Nmcli.ethernetDevices.length > 0) { root.session.ethernet.active = Nmcli.ethernetDevices[0]; } else if (Nmcli.networks.length > 0) { root.session.network.active = Nmcli.networks[0]; } } } } } CollapsibleSection { id: vpnListSection Layout.fillWidth: true title: qsTr("VPN") expanded: true Loader { Layout.fillWidth: true asynchronous: true sourceComponent: Component { VpnList { session: root.session showHeader: false } } } } CollapsibleSection { id: ethernetListSection Layout.fillWidth: true title: qsTr("Ethernet") expanded: true Loader { Layout.fillWidth: true asynchronous: true sourceComponent: Component { EthernetList { session: root.session showHeader: false } } } } CollapsibleSection { id: wirelessListSection Layout.fillWidth: true title: qsTr("Wireless") expanded: true Loader { Layout.fillWidth: true asynchronous: true sourceComponent: Component { WirelessList { session: root.session showHeader: false } } } } } } } rightContent: Component { Item { id: rightPaneItem property var vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null property var wirelessPane: root.session && root.session.network ? root.session.network.active : null property var pane: vpnPane || ethernetPane || wirelessPane property string paneId: vpnPane ? ("vpn:" + (vpnPane.name || "")) : (ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings")) property Component targetComponent: settingsComponent property Component nextComponent: settingsComponent function getComponentForPane() { if (vpnPane) return vpnDetailsComponent; if (ethernetPane) return ethernetDetailsComponent; if (wirelessPane) return wirelessDetailsComponent; return settingsComponent; } Component.onCompleted: { targetComponent = getComponentForPane(); nextComponent = targetComponent; } Connections { function onActiveChanged() { // Clear others when VPN is selected if (root.session && root.session.vpn && root.session.vpn.active) { if (root.session.ethernet && root.session.ethernet.active) root.session.ethernet.active = null; if (root.session.network && root.session.network.active) root.session.network.active = null; } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } target: root.session && root.session.vpn ? root.session.vpn : null enabled: target !== null } Connections { function onActiveChanged() { // Clear others when ethernet is selected if (root.session && root.session.ethernet && root.session.ethernet.active) { if (root.session.vpn && root.session.vpn.active) root.session.vpn.active = null; if (root.session.network && root.session.network.active) root.session.network.active = null; } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } target: root.session && root.session.ethernet ? root.session.ethernet : null enabled: target !== null } Connections { function onActiveChanged() { // Clear others when wireless is selected if (root.session && root.session.network && root.session.network.active) { if (root.session.vpn && root.session.vpn.active) root.session.vpn.active = null; if (root.session.ethernet && root.session.ethernet.active) root.session.ethernet.active = null; } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } target: root.session && root.session.network ? root.session.network : null enabled: target !== null } Loader { id: rightLoader anchors.fill: parent opacity: 1 scale: 1 transformOrigin: Item.Center clip: false asynchronous: true sourceComponent: rightPaneItem.targetComponent } Behavior on paneId { PaneTransition { target: rightLoader propertyActions: [ PropertyAction { target: rightPaneItem property: "targetComponent" value: rightPaneItem.nextComponent } ] } } } } } Component { id: settingsComponent StyledFlickable { id: settingsFlickable flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height StyledScrollBar.vertical: StyledScrollBar { flickable: settingsFlickable } NetworkSettings { id: settingsInner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session } } } Component { id: ethernetDetailsComponent StyledFlickable { id: ethernetFlickable flickableDirection: Flickable.VerticalFlick contentHeight: ethernetDetailsInner.height StyledScrollBar.vertical: StyledScrollBar { flickable: ethernetFlickable } EthernetDetails { id: ethernetDetailsInner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session } } } Component { id: wirelessDetailsComponent StyledFlickable { id: wirelessFlickable flickableDirection: Flickable.VerticalFlick contentHeight: wirelessDetailsInner.height StyledScrollBar.vertical: StyledScrollBar { flickable: wirelessFlickable } WirelessDetails { id: wirelessDetailsInner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session } } } Component { id: vpnDetailsComponent StyledFlickable { id: vpnFlickable flickableDirection: Flickable.VerticalFlick contentHeight: vpnDetailsInner.height StyledScrollBar.vertical: StyledScrollBar { flickable: vpnFlickable } VpnDetails { id: vpnDetailsInner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top session: root.session } } } WirelessPasswordDialog { anchors.fill: parent session: root.session z: 1000 } } ================================================ FILE: modules/controlcenter/network/VpnDetails.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config import QtQuick import QtQuick.Controls import QtQuick.Layouts DeviceDetails { id: root required property Session session readonly property var vpnProvider: root.session.vpn.active readonly property bool providerEnabled: { if (!vpnProvider || vpnProvider.index === undefined) return false; const provider = Config.utilities.vpn.provider[vpnProvider.index]; return provider && typeof provider === "object" && provider.enabled === true; } device: vpnProvider headerComponent: Component { ConnectionHeader { icon: "vpn_key" title: root.vpnProvider?.displayName ?? qsTr("Unknown") } } sections: [ Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Connection status") description: qsTr("VPN connection settings") } SectionContainer { ToggleRow { label: qsTr("Enable this provider") checked: root.providerEnabled toggle.onToggled: { if (!root.vpnProvider) return; const providers = []; const index = root.vpnProvider.index; // Copy providers and update enabled state for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { const p = Config.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, displayName: p.displayName, interface: p.interface }; if (checked) { // Enable this one, disable others newProvider.enabled = (i === index); } else { // Just disable this one newProvider.enabled = (i === index) ? false : (p.enabled !== false); } providers.push(newProvider); } else { providers.push(p); } } Config.utilities.vpn.provider = providers; Config.save(); } } RowLayout { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal spacing: Appearance.spacing.normal TextButton { Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 visible: root.providerEnabled enabled: !VPN.connecting inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer text: VPN.connected ? qsTr("Disconnect") : qsTr("Connect") onClicked: { VPN.toggle(); } } TextButton { Layout.fillWidth: true text: qsTr("Edit Provider") inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer onClicked: { editVpnDialog.editIndex = root.vpnProvider.index; editVpnDialog.providerName = root.vpnProvider.name; editVpnDialog.displayName = root.vpnProvider.displayName; editVpnDialog.interfaceName = root.vpnProvider.interface; editVpnDialog.open(); } } TextButton { Layout.fillWidth: true text: qsTr("Delete Provider") inactiveColour: Colours.palette.m3errorContainer inactiveOnColour: Colours.palette.m3onErrorContainer onClicked: { const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { if (i !== root.vpnProvider.index) { providers.push(Config.utilities.vpn.provider[i]); } } Config.utilities.vpn.provider = providers; Config.save(); root.session.vpn.active = null; } } } } } }, Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Provider details") description: qsTr("VPN provider information") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Provider") value: root.vpnProvider?.name ?? qsTr("Unknown") } PropertyRow { showTopMargin: true label: qsTr("Display name") value: root.vpnProvider?.displayName ?? qsTr("Unknown") } PropertyRow { showTopMargin: true label: qsTr("Interface") value: root.vpnProvider?.interface || qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Status") value: { if (!root.providerEnabled) return qsTr("Disabled"); if (VPN.connecting) return qsTr("Connecting..."); if (VPN.connected) return qsTr("Connected"); return qsTr("Enabled (Not connected)"); } } PropertyRow { showTopMargin: true label: qsTr("Enabled") value: root.providerEnabled ? qsTr("Yes") : qsTr("No") } } } } ] // Edit VPN Dialog Popup { id: editVpnDialog property int editIndex: -1 property string providerName: "" property string displayName: "" property string interfaceName: "" function closeWithAnimation(): void { close(); } parent: Overlay.overlay anchors.centerIn: parent width: Math.min(400, parent.width - Appearance.padding.large * 2) padding: Appearance.padding.large * 1.5 modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside opacity: 0 scale: 0.7 enter: Transition { Anim { property: "opacity" from: 0 to: 1 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { property: "scale" from: 0.7 to: 1 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } exit: Transition { Anim { property: "opacity" from: 1 to: 0 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { property: "scale" from: 1 to: 0.7 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) } background: StyledRect { color: Colours.palette.m3surfaceContainerHigh radius: Appearance.rounding.large Elevation { anchors.fill: parent radius: parent.radius level: 3 z: -1 } } contentItem: ColumnLayout { spacing: Appearance.spacing.normal StyledText { text: qsTr("Edit VPN Provider") font.pointSize: Appearance.font.size.large font.weight: 500 } ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller / 2 StyledText { text: qsTr("Display Name") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } StyledRect { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) Behavior on color { CAnim {} } Behavior on border.color { CAnim {} } StyledTextField { id: displayNameField anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.displayName onTextChanged: editVpnDialog.displayName = text } } } ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } StyledRect { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) Behavior on color { CAnim {} } Behavior on border.color { CAnim {} } StyledTextField { id: interfaceNameField anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.interfaceName onTextChanged: editVpnDialog.interfaceName = text } } } RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true spacing: Appearance.spacing.normal TextButton { Layout.fillWidth: true text: qsTr("Cancel") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: editVpnDialog.closeWithAnimation() } TextButton { Layout.fillWidth: true text: qsTr("Save") enabled: editVpnDialog.interfaceName.length > 0 inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer onClicked: { const providers = []; const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex]; const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { if (i === editVpnDialog.editIndex) { providers.push({ name: editVpnDialog.providerName, displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, interface: editVpnDialog.interfaceName, enabled: wasEnabled }); } else { providers.push(Config.utilities.vpn.provider[i]); } } Config.utilities.vpn.provider = providers; Config.save(); editVpnDialog.closeWithAnimation(); } } } } } } ================================================ FILE: modules/controlcenter/network/VpnList.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Controls import QtQuick.Layouts ColumnLayout { id: root required property Session session property bool showHeader: true property int pendingSwitchIndex: -1 spacing: Appearance.spacing.normal Connections { function onConnectedChanged() { if (!VPN.connected && root.pendingSwitchIndex >= 0) { const targetIndex = root.pendingSwitchIndex; root.pendingSwitchIndex = -1; const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { const p = Config.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, displayName: p.displayName, interface: p.interface, enabled: (i === targetIndex) }; providers.push(newProvider); } else { providers.push(p); } } Config.utilities.vpn.provider = providers; Config.save(); Qt.callLater(function () { VPN.toggle(); }); } } target: VPN } TextButton { Layout.fillWidth: true text: qsTr("+ Add VPN Provider") inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer onClicked: { vpnDialog.showProviderSelection(); } } ListView { id: listView Layout.fillWidth: true Layout.preferredHeight: contentHeight interactive: false spacing: Appearance.spacing.smaller model: ScriptModel { values: Config.utilities.vpn.provider.map((provider, index) => { const isObject = typeof provider === "object"; const name = isObject ? (provider.name || "custom") : String(provider); const displayName = isObject ? (provider.displayName || name) : name; const iface = isObject ? (provider.interface || "") : ""; const enabled = isObject ? (provider.enabled === true) : false; return { index: index, name: name, displayName: displayName, interface: iface, provider: provider, enabled: enabled }; }) } delegate: Component { StyledRect { required property var modelData required property int index width: ListView.view ? ListView.view.width : undefined color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { function onClicked(): void { if (root.session && root.session.vpn) { root.session.vpn.active = modelData; } } } RowLayout { id: rowLayout anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledRect { implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { id: icon anchors.centerIn: parent text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" font.pointSize: Appearance.font.size.large fill: modelData.enabled && VPN.connected ? 1 : 0 color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { Layout.fillWidth: true elide: Text.ElideRight maximumLineCount: 1 text: modelData.displayName || qsTr("Unknown") } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { Layout.fillWidth: true text: { if (modelData.enabled && VPN.connected) return qsTr("Connected"); if (modelData.enabled && VPN.connecting) return qsTr("Connecting..."); if (modelData.enabled) return qsTr("Enabled"); return qsTr("Disabled"); } color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline font.pointSize: Appearance.font.size.small font.weight: modelData.enabled && VPN.connected ? 500 : 400 elide: Text.ElideRight } } } StyledRect { implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) StateLayer { function onClicked(): void { const clickedIndex = modelData.index; if (modelData.enabled) { VPN.toggle(); } else { if (VPN.connected) { root.pendingSwitchIndex = clickedIndex; VPN.toggle(); } else { const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { const p = Config.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, displayName: p.displayName, interface: p.interface, enabled: (i === clickedIndex) }; providers.push(newProvider); } else { providers.push(p); } } Config.utilities.vpn.provider = providers; Config.save(); Qt.callLater(function () { VPN.toggle(); }); } } } enabled: !VPN.connecting } MaterialIcon { id: connectIcon anchors.centerIn: parent text: VPN.connected && modelData.enabled ? "link_off" : "link" color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } StyledRect { implicitWidth: implicitHeight implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full color: "transparent" StateLayer { function onClicked(): void { const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { if (i !== modelData.index) { providers.push(Config.utilities.vpn.provider[i]); } } Config.utilities.vpn.provider = providers; Config.save(); } } MaterialIcon { id: deleteIcon anchors.centerIn: parent text: "delete" color: Colours.palette.m3onSurface } } } implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } } Popup { id: vpnDialog property string currentState: "selection" property int editIndex: -1 property string providerName: "" property string displayName: "" property string interfaceName: "" function showProviderSelection(): void { currentState = "selection"; open(); } function closeWithAnimation(): void { close(); } function showAddForm(providerType: string, defaultDisplayName: string): void { editIndex = -1; providerName = providerType; displayName = defaultDisplayName; interfaceName = ""; if (currentState === "selection") { transitionToForm.start(); } else { currentState = "form"; isClosing = false; open(); } } function showEditForm(index: int): void { const provider = Config.utilities.vpn.provider[index]; const isObject = typeof provider === "object"; editIndex = index; providerName = isObject ? (provider.name || "custom") : String(provider); displayName = isObject ? (provider.displayName || providerName) : providerName; interfaceName = isObject ? (provider.interface || "") : ""; currentState = "form"; open(); } parent: Overlay.overlay x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) padding: Appearance.padding.large * 1.5 modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside opacity: 0 scale: 0.7 enter: Transition { ParallelAnimation { Anim { property: "opacity" from: 0 to: 1 duration: Appearance.anim.durations.normal easing.bezierCurve: Appearance.anim.curves.emphasized } Anim { property: "scale" from: 0.7 to: 1 duration: Appearance.anim.durations.normal easing.bezierCurve: Appearance.anim.curves.emphasized } } } exit: Transition { ParallelAnimation { Anim { property: "opacity" from: 1 to: 0 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.emphasized } Anim { property: "scale" from: 1 to: 0.7 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.emphasized } } } Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) } onClosed: { currentState = "selection"; } background: StyledRect { color: Colours.palette.m3surfaceContainerHigh radius: Appearance.rounding.large Elevation { anchors.fill: parent radius: parent.radius level: 3 z: -1 } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.normal easing.bezierCurve: Appearance.anim.curves.emphasized } } } contentItem: Item { implicitHeight: vpnDialog.currentState === "selection" ? selectionContent.implicitHeight : formContent.implicitHeight Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.normal easing.bezierCurve: Appearance.anim.curves.emphasized } } ColumnLayout { id: selectionContent anchors.fill: parent spacing: Appearance.spacing.normal visible: vpnDialog.currentState === "selection" opacity: vpnDialog.currentState === "selection" ? 1 : 0 Behavior on opacity { Anim { duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.emphasized } } StyledText { text: qsTr("Add VPN Provider") font.pointSize: Appearance.font.size.large font.weight: 500 } StyledText { Layout.fillWidth: true text: qsTr("Choose a provider to add") wrapMode: Text.WordWrap color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } TextButton { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true text: qsTr("NetBird") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { providers.push(Config.utilities.vpn.provider[i]); } providers.push({ name: "netbird", displayName: "NetBird", interface: "wt0" }); Config.utilities.vpn.provider = providers; Config.save(); vpnDialog.closeWithAnimation(); } } TextButton { Layout.fillWidth: true text: qsTr("Tailscale") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { providers.push(Config.utilities.vpn.provider[i]); } providers.push({ name: "tailscale", displayName: "Tailscale", interface: "tailscale0" }); Config.utilities.vpn.provider = providers; Config.save(); vpnDialog.closeWithAnimation(); } } TextButton { Layout.fillWidth: true text: qsTr("Cloudflare WARP") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { providers.push(Config.utilities.vpn.provider[i]); } providers.push({ name: "warp", displayName: "Cloudflare WARP", interface: "CloudflareWARP" }); Config.utilities.vpn.provider = providers; Config.save(); vpnDialog.closeWithAnimation(); } } TextButton { Layout.fillWidth: true text: qsTr("WireGuard (Custom)") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { vpnDialog.showAddForm("wireguard", "WireGuard"); } } TextButton { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true text: qsTr("Cancel") inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer onClicked: vpnDialog.closeWithAnimation() } } ColumnLayout { id: formContent anchors.fill: parent spacing: Appearance.spacing.normal visible: vpnDialog.currentState === "form" opacity: vpnDialog.currentState === "form" ? 1 : 0 Behavior on opacity { Anim { duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.emphasized } } StyledText { text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) font.pointSize: Appearance.font.size.large font.weight: 500 } ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller / 2 StyledText { text: qsTr("Display Name") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } StyledRect { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) Behavior on color { CAnim {} } Behavior on border.color { CAnim {} } StyledTextField { id: displayNameField anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.displayName onTextChanged: vpnDialog.displayName = text } } } ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } StyledRect { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.small border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) Behavior on color { CAnim {} } Behavior on border.color { CAnim {} } StyledTextField { id: interfaceNameField anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.interfaceName onTextChanged: vpnDialog.interfaceName = text } } } RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true spacing: Appearance.spacing.normal TextButton { Layout.fillWidth: true text: qsTr("Cancel") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: vpnDialog.closeWithAnimation() } TextButton { Layout.fillWidth: true text: qsTr("Save") enabled: vpnDialog.interfaceName.length > 0 inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer onClicked: { const providers = []; const newProvider = { name: vpnDialog.providerName, displayName: vpnDialog.displayName || vpnDialog.interfaceName, interface: vpnDialog.interfaceName }; if (vpnDialog.editIndex >= 0) { for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { if (i === vpnDialog.editIndex) { providers.push(newProvider); } else { providers.push(Config.utilities.vpn.provider[i]); } } } else { for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { providers.push(Config.utilities.vpn.provider[i]); } providers.push(newProvider); } Config.utilities.vpn.provider = providers; Config.save(); vpnDialog.closeWithAnimation(); } } } } } SequentialAnimation { id: transitionToForm ParallelAnimation { Anim { target: selectionContent property: "opacity" to: 0 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.emphasized } } ScriptAction { script: { vpnDialog.currentState = "form"; } } ParallelAnimation { Anim { target: formContent property: "opacity" to: 1 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.emphasized } } } } } ================================================ FILE: modules/controlcenter/network/VpnSettings.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property Session session spacing: Appearance.spacing.normal SettingsHeader { icon: "vpn_key" title: qsTr("VPN Settings") } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("General") description: qsTr("VPN configuration") } SectionContainer { ToggleRow { label: qsTr("VPN enabled") checked: Config.utilities.vpn.enabled toggle.onToggled: { Config.utilities.vpn.enabled = checked; Config.save(); } } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Providers") description: qsTr("Manage VPN providers") } SectionContainer { contentSpacing: Appearance.spacing.normal ListView { Layout.fillWidth: true Layout.preferredHeight: contentHeight interactive: false spacing: Appearance.spacing.smaller model: ScriptModel { values: Config.utilities.vpn.provider.map((provider, index) => { const isObject = typeof provider === "object"; const name = isObject ? (provider.name || "custom") : String(provider); const displayName = isObject ? (provider.displayName || name) : name; const iface = isObject ? (provider.interface || "") : ""; return { index: index, name: name, displayName: displayName, interface: iface, provider: provider, isActive: index === 0 }; }) } delegate: Component { StyledRect { required property var modelData required property int index width: ListView.view ? ListView.view.width : undefined color: Colours.tPalette.m3surfaceContainerHigh radius: Appearance.rounding.normal RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal MaterialIcon { text: modelData.isActive ? "vpn_key" : "vpn_key_off" font.pointSize: Appearance.font.size.large color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { text: modelData.displayName font.weight: modelData.isActive ? 500 : 400 } StyledText { text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface")) font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline } } IconButton { icon: modelData.isActive ? "arrow_downward" : "arrow_upward" visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1 onClicked: { if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) { // Move down const providers = [...Config.utilities.vpn.provider]; const temp = providers[index]; providers[index] = providers[index + 1]; providers[index + 1] = temp; Config.utilities.vpn.provider = providers; Config.save(); } else if (!modelData.isActive) { // Make active (move to top) const providers = [...Config.utilities.vpn.provider]; const provider = providers.splice(index, 1)[0]; providers.unshift(provider); Config.utilities.vpn.provider = providers; Config.save(); } } } IconButton { icon: "delete" onClicked: { const providers = [...Config.utilities.vpn.provider]; providers.splice(index, 1); Config.utilities.vpn.provider = providers; Config.save(); } } } implicitHeight: 60 } } } TextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal text: qsTr("+ Add Provider") inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer onClicked: { addProviderDialog.open(); } } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Quick Add") description: qsTr("Add common VPN providers") } SectionContainer { contentSpacing: Appearance.spacing.smaller TextButton { Layout.fillWidth: true text: qsTr("+ Add NetBird") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = [...Config.utilities.vpn.provider]; providers.push({ name: "netbird", displayName: "NetBird", interface: "wt0" }); Config.utilities.vpn.provider = providers; Config.save(); } } TextButton { Layout.fillWidth: true text: qsTr("+ Add Tailscale") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = [...Config.utilities.vpn.provider]; providers.push({ name: "tailscale", displayName: "Tailscale", interface: "tailscale0" }); Config.utilities.vpn.provider = providers; Config.save(); } } TextButton { Layout.fillWidth: true text: qsTr("+ Add Cloudflare WARP") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = [...Config.utilities.vpn.provider]; providers.push({ name: "warp", displayName: "Cloudflare WARP", interface: "CloudflareWARP" }); Config.utilities.vpn.provider = providers; Config.save(); } } } } ================================================ FILE: modules/controlcenter/network/WirelessDetails.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import QtQuick import QtQuick.Layouts DeviceDetails { id: root required property Session session readonly property var network: root.session.network.active function checkSavedProfile(): void { if (network && network.ssid) { Nmcli.loadSavedConnections(() => {}); } } function updateDeviceDetails(): void { if (network && network.ssid) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); if (isActive) { Nmcli.getWirelessDeviceDetails(""); } else { Nmcli.wirelessDeviceDetails = null; } } else { Nmcli.wirelessDeviceDetails = null; } } device: network Component.onCompleted: { updateDeviceDetails(); checkSavedProfile(); } onNetworkChanged: { connectionUpdateTimer.stop(); if (network && network.ssid) { connectionUpdateTimer.start(); } updateDeviceDetails(); checkSavedProfile(); } headerComponent: Component { ConnectionHeader { icon: root.network?.isSecure ? "lock" : "wifi" title: root.network?.ssid ?? qsTr("Unknown") } } sections: [ Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Connection status") description: qsTr("Connection settings for this network") } SectionContainer { ToggleRow { label: qsTr("Connected") checked: root.network?.active ?? false toggle.onToggled: { if (checked) { NetworkConnection.handleConnect(root.network, root.session, null); } else { Nmcli.disconnectFromNetwork(); } } } TextButton { Layout.fillWidth: true Layout.topMargin: Appearance.spacing.normal Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 visible: { if (!root.network || !root.network.ssid) { return false; } return Nmcli.hasSavedProfile(root.network.ssid); } inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Forget Network") onClicked: { if (root.network && root.network.ssid) { if (root.network.active) { Nmcli.disconnectFromNetwork(); } Nmcli.forgetNetwork(root.network.ssid); } } } } } }, Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Network properties") description: qsTr("Additional information") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("SSID") value: root.network?.ssid ?? qsTr("Unknown") } PropertyRow { showTopMargin: true label: qsTr("BSSID") value: root.network?.bssid ?? qsTr("Unknown") } PropertyRow { showTopMargin: true label: qsTr("Signal strength") value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Frequency") value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Security") value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A") } } } }, Component { ColumnLayout { spacing: Appearance.spacing.normal SectionHeader { title: qsTr("Connection information") description: qsTr("Network connection details") } SectionContainer { ConnectionInfoSection { deviceDetails: Nmcli.wirelessDeviceDetails } } } } ] Connections { function onActiveChanged() { root.updateDeviceDetails(); } function onWirelessDeviceDetailsChanged() { if (root.network && root.network.ssid) { const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid); if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { connectionUpdateTimer.stop(); } } } target: Nmcli } Timer { id: connectionUpdateTimer interval: 500 repeat: true running: root.network && root.network.ssid onTriggered: { if (root.network) { const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid); if (isActive) { if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { Nmcli.getWirelessDeviceDetails("", () => {}); } else { connectionUpdateTimer.stop(); } } else { if (Nmcli.wirelessDeviceDetails !== null) { Nmcli.wirelessDeviceDetails = null; } } } } } } ================================================ FILE: modules/controlcenter/network/WirelessList.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import Quickshell import QtQuick import QtQuick.Layouts DeviceList { id: root required property Session session function checkSavedProfileForNetwork(ssid: string): void { if (ssid && ssid.length > 0) { Nmcli.loadSavedConnections(() => {}); } } title: qsTr("Networks (%1)").arg(Nmcli.networks.length) description: qsTr("All available WiFi networks") activeItem: session.network.active titleSuffix: Component { StyledText { visible: Nmcli.scanning text: qsTr("Scanning...") color: Colours.palette.m3primary font.pointSize: Appearance.font.size.small } } model: ScriptModel { values: [...Nmcli.networks].sort((a, b) => { if (a.active !== b.active) return b.active - a.active; return b.strength - a.strength; }) } headerComponent: Component { RowLayout { spacing: Appearance.spacing.smaller StyledText { text: qsTr("Settings") font.pointSize: Appearance.font.size.large font.weight: 500 } Item { Layout.fillWidth: true } ToggleButton { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller onClicked: { Nmcli.toggleWifi(null); } } ToggleButton { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller onClicked: { Nmcli.rescanWifi(); } } ToggleButton { toggled: !root.session.network.active icon: "settings" accent: "Primary" iconSize: Appearance.font.size.normal horizontalPadding: Appearance.padding.normal verticalPadding: Appearance.padding.smaller onClicked: { if (root.session.network.active) root.session.network.active = null; else { root.session.network.active = root.view.model.get(0)?.modelData ?? null; } } } } } delegate: Component { StyledRect { id: networkDelegate required property var modelData width: ListView.view ? ListView.view.width : undefined color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === networkDelegate.modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { function onClicked(): void { root.session.network.active = networkDelegate.modelData; if (networkDelegate.modelData && networkDelegate.modelData.ssid) { root.checkSavedProfileForNetwork(networkDelegate.modelData.ssid); } } } RowLayout { id: rowLayout anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal StyledRect { implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: networkDelegate.modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { id: icon anchors.centerIn: parent text: Icons.getNetworkIcon(networkDelegate.modelData.strength, networkDelegate.modelData.isSecure) font.pointSize: Appearance.font.size.large fill: networkDelegate.modelData.active ? 1 : 0 color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { Layout.fillWidth: true elide: Text.ElideRight maximumLineCount: 1 text: networkDelegate.modelData.ssid || qsTr("Unknown") } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { Layout.fillWidth: true text: { if (networkDelegate.modelData.active) return qsTr("Connected"); if (networkDelegate.modelData.isSecure && networkDelegate.modelData.security && networkDelegate.modelData.security.length > 0) { return networkDelegate.modelData.security; } if (networkDelegate.modelData.isSecure) return qsTr("Secured"); return qsTr("Open"); } color: networkDelegate.modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline font.pointSize: Appearance.font.size.small font.weight: networkDelegate.modelData.active ? 500 : 400 elide: Text.ElideRight } } } StyledRect { implicitWidth: implicitHeight implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, networkDelegate.modelData.active ? 1 : 0) StateLayer { function onClicked(): void { if (networkDelegate.modelData.active) { Nmcli.disconnectFromNetwork(); } else { NetworkConnection.handleConnect(networkDelegate.modelData, root.session, null); } } } MaterialIcon { id: connectIcon anchors.centerIn: parent text: networkDelegate.modelData.active ? "link_off" : "link" color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } } implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } onItemSelected: function (item) { session.network.active = item; if (item && item.ssid) { checkSavedProfileForNetwork(item.ssid); } } } ================================================ FILE: modules/controlcenter/network/WirelessPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components.containers import QtQuick SplitPaneWithDetails { id: root required property Session session anchors.fill: parent activeItem: session.network.active paneIdGenerator: function (item) { return item ? (item.ssid || item.bssid || "") : ""; } leftContent: Component { WirelessList { session: root.session } } rightDetailsComponent: Component { WirelessDetails { session: root.session } } rightSettingsComponent: Component { StyledFlickable { flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height clip: true WirelessSettings { id: settingsInner anchors.left: parent.left anchors.right: parent.right session: root.session } } } overlayComponent: Component { WirelessPasswordDialog { anchors.fill: parent session: root.session } } } ================================================ FILE: modules/controlcenter/network/WirelessPasswordDialog.qml ================================================ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import Quickshell import QtQuick import QtQuick.Layouts Item { id: root required property Session session readonly property var network: { if (session.network.pendingNetwork) { return session.network.pendingNetwork; } if (session.network.active) { return session.network.active; } return null; } property bool isClosing: false function checkConnectionStatus(): void { if (!root.visible || !connectButton.connecting) { return; } const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (isConnected) { connectionSuccessTimer.start(); return; } if (Nmcli.pendingConnection === null && connectButton.connecting) { if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } } } function closeDialog(): void { if (isClosing) { return; } isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; connectButton.hasError = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); } visible: session.network.showPasswordDialog || isClosing enabled: session.network.showPasswordDialog && !isClosing focus: enabled Keys.onEscapePressed: closeDialog() Rectangle { anchors.fill: parent color: Qt.rgba(0, 0, 0, 0.5) opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 Behavior on opacity { Anim {} } MouseArea { anchors.fill: parent onClicked: root.closeDialog() } } StyledRect { id: dialog anchors.centerIn: parent implicitWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surface opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 Behavior on opacity { Anim {} } Behavior on scale { Anim {} } ParallelAnimation { running: root.isClosing onFinished: { if (root.isClosing) { root.session.network.showPasswordDialog = false; root.isClosing = false; } } Anim { target: dialog property: "opacity" to: 0 } Anim { target: dialog property: "scale" to: 0.7 } } Keys.onEscapePressed: root.closeDialog() ColumnLayout { id: content anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" font.pointSize: Appearance.font.size.extraLarge * 2 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") font.pointSize: Appearance.font.size.large font.weight: 500 } StyledText { Layout.alignment: Qt.AlignHCenter text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledText { id: statusText Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small visible: connectButton.connecting || connectButton.hasError text: { if (connectButton.hasError) { return qsTr("Connection failed. Please check your password and try again."); } if (connectButton.connecting) { return qsTr("Connecting..."); } return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small font.weight: 400 wrapMode: Text.WordWrap Layout.maximumWidth: parent.width - Appearance.padding.large * 2 } Item { id: passwordContainer property string passwordBuffer: "" Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) focus: true Keys.onPressed: event => { if (!activeFocus) { forceActiveFocus(); } if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); } event.accepted = true; } else if (event.key === Qt.Key_Backspace) { if (event.modifiers & Qt.ControlModifier) { passwordBuffer = ""; } else { passwordBuffer = passwordBuffer.slice(0, -1); } event.accepted = true; } else if (event.text && event.text.length > 0) { passwordBuffer += event.text; event.accepted = true; } } Connections { function onShowPasswordDialogChanged(): void { if (root.session.network.showPasswordDialog) { Qt.callLater(() => { passwordContainer.forceActiveFocus(); passwordContainer.passwordBuffer = ""; connectButton.hasError = false; }); } } target: root.session.network } Connections { function onVisibleChanged(): void { if (root.visible) { Qt.callLater(() => { passwordContainer.forceActiveFocus(); }); } } target: root } StyledRect { anchors.fill: parent radius: Appearance.rounding.normal color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) border.color: { if (connectButton.hasError) { return Colours.palette.m3error; } if (passwordContainer.activeFocus) { return Colours.palette.m3primary; } return root.visible ? Colours.palette.m3outline : "transparent"; } Behavior on border.color { CAnim {} } Behavior on border.width { CAnim {} } Behavior on color { CAnim {} } } StateLayer { function onClicked(): void { passwordContainer.forceActiveFocus(); } hoverEnabled: false cursorShape: Qt.IBeamCursor } StyledText { id: placeholder anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal font.family: Appearance.font.family.mono opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { Anim {} } } ListView { id: charList readonly property int fullWidth: count * (implicitHeight + spacing) - spacing anchors.centerIn: parent implicitWidth: fullWidth implicitHeight: Appearance.font.size.normal orientation: Qt.Horizontal spacing: Appearance.spacing.small / 2 interactive: false model: ScriptModel { values: passwordContainer.passwordBuffer.split("") } delegate: StyledRect { id: ch implicitWidth: implicitHeight implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface radius: Appearance.rounding.small / 2 opacity: 0 scale: 0 Component.onCompleted: { opacity = 1; scale = 1; } ListView.onRemove: removeAnim.start() SequentialAnimation { id: removeAnim PropertyAction { target: ch property: "ListView.delayRemove" value: true } ParallelAnimation { Anim { target: ch property: "opacity" to: 0 } Anim { target: ch property: "scale" to: 0.5 } } PropertyAction { target: ch property: "ListView.delayRemove" value: false } } Behavior on opacity { Anim {} } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } Behavior on implicitWidth { Anim {} } } } RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true spacing: Appearance.spacing.normal TextButton { id: cancelButton Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") onClicked: root.closeDialog() } TextButton { id: connectButton property bool connecting: false property bool hasError: false Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") enabled: passwordContainer.passwordBuffer.length > 0 && !connecting onClicked: { if (!root.network || connecting) { return; } const password = passwordContainer.passwordBuffer; if (!password || password.length === 0) { return; } hasError = false; connecting = true; enabled = false; text = qsTr("Connecting..."); NetworkConnection.connectWithPassword(root.network, password, result => { if (result && result.success) {} else if (result && result.needsPassword) { connectionMonitor.stop(); connecting = false; hasError = true; enabled = true; text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } else { connectionMonitor.stop(); connecting = false; hasError = true; enabled = true; text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; if (root.network && root.network.ssid) { Nmcli.forgetNetwork(root.network.ssid); } } }); connectionMonitor.start(); } } } } } Timer { id: connectionMonitor property int repeatCount: 0 interval: 1000 repeat: true triggeredOnStart: false onTriggered: { repeatCount++; root.checkConnectionStatus(); } onRunningChanged: { if (!running) { repeatCount = 0; } } } Timer { id: connectionSuccessTimer interval: 500 onTriggered: { if (root.visible && Nmcli.active && Nmcli.active.ssid) { const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); if (stillConnected) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.text = qsTr("Connect"); root.closeDialog(); } } } } Connections { function onActiveChanged() { if (root.visible) { root.checkConnectionStatus(); } } function onConnectionFailed(ssid: string) { if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); connectButton.connecting = false; connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); passwordContainer.passwordBuffer = ""; Nmcli.forgetNetwork(ssid); } } target: Nmcli } } ================================================ FILE: modules/controlcenter/network/WirelessSettings.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property Session session spacing: Appearance.spacing.normal SettingsHeader { icon: "wifi" title: qsTr("Network settings") } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("WiFi status") description: qsTr("General WiFi settings") } SectionContainer { ToggleRow { label: qsTr("WiFi enabled") checked: Nmcli.wifiEnabled toggle.onToggled: { Nmcli.enableWifi(checked); } } } SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Network information") description: qsTr("Current network connection") } SectionContainer { contentSpacing: Appearance.spacing.small / 2 PropertyRow { label: qsTr("Connected network") value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected") } PropertyRow { showTopMargin: true label: qsTr("Signal strength") value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Security") value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A") } PropertyRow { showTopMargin: true label: qsTr("Frequency") value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A") } } } ================================================ FILE: modules/controlcenter/state/BluetoothState.qml ================================================ import Quickshell.Bluetooth import QtQuick QtObject { id: root property BluetoothDevice active: null property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter property bool editingAdapterName: false property bool fabMenuOpen: false property bool editingDeviceName: false } ================================================ FILE: modules/controlcenter/state/EthernetState.qml ================================================ import QtQuick QtObject { id: root property var active: null } ================================================ FILE: modules/controlcenter/state/LauncherState.qml ================================================ import QtQuick QtObject { id: root property var active: null } ================================================ FILE: modules/controlcenter/state/NetworkState.qml ================================================ import QtQuick QtObject { id: root property var active: null property bool showPasswordDialog: false property var pendingNetwork: null } ================================================ FILE: modules/controlcenter/state/VpnState.qml ================================================ import QtQuick QtObject { property var active: null } ================================================ FILE: modules/controlcenter/taskbar/TaskbarPane.qml ================================================ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components import qs.components.controls import qs.components.effects import qs.components.containers import qs.services import qs.config import qs.utils import Quickshell.Widgets import QtQuick import QtQuick.Layouts Item { id: root required property Session session property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false property bool clockShowIcon: Config.bar.clock.showIcon ?? true property bool clockBackground: Config.bar.clock.background ?? false property bool clockShowDate: Config.bar.clock.showDate ?? false property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true property int dragThreshold: Config.bar.dragThreshold ?? 20 property bool showAudio: Config.bar.status.showAudio ?? true property bool showMicrophone: Config.bar.status.showMicrophone ?? true property bool showKbLayout: Config.bar.status.showKbLayout ?? false property bool showNetwork: Config.bar.status.showNetwork ?? true property bool showWifi: Config.bar.status.showWifi ?? true property bool showBluetooth: Config.bar.status.showBluetooth ?? true property bool showBattery: Config.bar.status.showBattery ?? true property bool showLockStatus: Config.bar.status.showLockStatus ?? true property bool trayBackground: Config.bar.tray.background ?? false property bool trayCompact: Config.bar.tray.compact ?? false property bool trayRecolour: Config.bar.tray.recolour ?? false property int workspacesShown: Config.bar.workspaces.shown ?? 5 property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true property bool scrollVolume: Config.bar.scrollActions.volume ?? true property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true property bool popoutTray: Config.bar.popouts.tray ?? true property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true property list monitorNames: Hypr.monitorNames() property list excludedScreens: Config.bar.excludedScreens ?? [] function saveConfig(entryIndex, entryEnabled) { Config.bar.activeWindow.compact = root.activeWindowCompact; Config.bar.activeWindow.inverted = root.activeWindowInverted; Config.bar.clock.background = root.clockBackground; Config.bar.clock.showDate = root.clockShowDate; Config.bar.clock.showIcon = root.clockShowIcon; Config.bar.persistent = root.persistent; Config.bar.showOnHover = root.showOnHover; Config.bar.dragThreshold = root.dragThreshold; Config.bar.status.showAudio = root.showAudio; Config.bar.status.showMicrophone = root.showMicrophone; Config.bar.status.showKbLayout = root.showKbLayout; Config.bar.status.showNetwork = root.showNetwork; Config.bar.status.showWifi = root.showWifi; Config.bar.status.showBluetooth = root.showBluetooth; Config.bar.status.showBattery = root.showBattery; Config.bar.status.showLockStatus = root.showLockStatus; Config.bar.tray.background = root.trayBackground; Config.bar.tray.compact = root.trayCompact; Config.bar.tray.recolour = root.trayRecolour; Config.bar.workspaces.shown = root.workspacesShown; Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; Config.bar.workspaces.showWindows = root.workspacesShowWindows; Config.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; Config.bar.scrollActions.workspaces = root.scrollWorkspaces; Config.bar.scrollActions.volume = root.scrollVolume; Config.bar.scrollActions.brightness = root.scrollBrightness; Config.bar.popouts.activeWindow = root.popoutActiveWindow; Config.bar.popouts.tray = root.popoutTray; Config.bar.popouts.statusIcons = root.popoutStatusIcons; Config.bar.excludedScreens = root.excludedScreens; const entries = []; for (let i = 0; i < entriesModel.count; i++) { const entry = entriesModel.get(i); let enabled = entry.enabled; if (entryIndex !== undefined && i === entryIndex) { enabled = entryEnabled; } entries.push({ id: entry.id, enabled: enabled }); } Config.bar.entries = entries; Config.save(); } anchors.fill: parent Component.onCompleted: { if (Config.bar.entries) { entriesModel.clear(); for (let i = 0; i < Config.bar.entries.length; i++) { const entry = Config.bar.entries[i]; entriesModel.append({ id: entry.id, enabled: entry.enabled !== false }); } } } ListModel { id: entriesModel } ClippingRectangle { id: taskbarClippingRect anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 anchors.rightMargin: Appearance.padding.normal radius: taskbarBorder.innerRadius color: "transparent" Loader { id: taskbarLoader anchors.fill: parent anchors.margins: Appearance.padding.large + Appearance.padding.normal anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large asynchronous: true sourceComponent: taskbarContentComponent } } InnerBorder { id: taskbarBorder leftThickness: 0 rightThickness: Appearance.padding.normal } Component { id: taskbarContentComponent StyledFlickable { id: sidebarFlickable flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: sidebarFlickable } ColumnLayout { id: sidebarLayout anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: Appearance.spacing.normal RowLayout { spacing: Appearance.spacing.smaller StyledText { text: qsTr("Taskbar") font.pointSize: Appearance.font.size.large font.weight: 500 } } SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Status Icons") font.pointSize: Appearance.font.size.normal } ConnectedButtonGroup { rootItem: root options: [ { label: qsTr("Speakers"), propertyName: "showAudio", onToggled: function (checked) { root.showAudio = checked; root.saveConfig(); } }, { label: qsTr("Microphone"), propertyName: "showMicrophone", onToggled: function (checked) { root.showMicrophone = checked; root.saveConfig(); } }, { label: qsTr("Keyboard"), propertyName: "showKbLayout", onToggled: function (checked) { root.showKbLayout = checked; root.saveConfig(); } }, { label: qsTr("Network"), propertyName: "showNetwork", onToggled: function (checked) { root.showNetwork = checked; root.saveConfig(); } }, { label: qsTr("Wifi"), propertyName: "showWifi", onToggled: function (checked) { root.showWifi = checked; root.saveConfig(); } }, { label: qsTr("Bluetooth"), propertyName: "showBluetooth", onToggled: function (checked) { root.showBluetooth = checked; root.saveConfig(); } }, { label: qsTr("Battery"), propertyName: "showBattery", onToggled: function (checked) { root.showBattery = checked; root.saveConfig(); } }, { label: qsTr("Capslock"), propertyName: "showLockStatus", onToggled: function (checked) { root.showLockStatus = checked; root.saveConfig(); } } ] } } RowLayout { id: mainRowLayout Layout.fillWidth: true spacing: Appearance.spacing.normal ColumnLayout { id: leftColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Workspaces") font.pointSize: Appearance.font.size.normal } StyledRect { Layout.fillWidth: true implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: workspacesShownRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Shown") } CustomSpinBox { min: 1 max: 20 value: root.workspacesShown onValueModified: value => { root.workspacesShown = value; root.saveConfig(); } } } } StyledRect { Layout.fillWidth: true implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: workspacesActiveIndicatorRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Active indicator") } StyledSwitch { checked: root.workspacesActiveIndicator onToggled: { root.workspacesActiveIndicator = checked; root.saveConfig(); } } } } StyledRect { Layout.fillWidth: true implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: workspacesOccupiedBgRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Occupied background") } StyledSwitch { checked: root.workspacesOccupiedBg onToggled: { root.workspacesOccupiedBg = checked; root.saveConfig(); } } } } StyledRect { Layout.fillWidth: true implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: workspacesShowWindowsRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Show windows") } StyledSwitch { checked: root.workspacesShowWindows onToggled: { root.workspacesShowWindows = checked; root.saveConfig(); } } } } StyledRect { Layout.fillWidth: true implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: workspacesMaxWindowIconsRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Max window icons") } CustomSpinBox { min: 0 max: 20 value: root.workspacesMaxWindowIcons onValueModified: value => { root.workspacesMaxWindowIcons = value; root.saveConfig(); } } } } StyledRect { Layout.fillWidth: true implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { Anim {} } RowLayout { id: workspacesPerMonitorRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Per monitor workspaces") } StyledSwitch { checked: root.workspacesPerMonitor onToggled: { root.workspacesPerMonitor = checked; root.saveConfig(); } } } } } SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Scroll Actions") font.pointSize: Appearance.font.size.normal } ConnectedButtonGroup { rootItem: root options: [ { label: qsTr("Workspaces"), propertyName: "scrollWorkspaces", onToggled: function (checked) { root.scrollWorkspaces = checked; root.saveConfig(); } }, { label: qsTr("Volume"), propertyName: "scrollVolume", onToggled: function (checked) { root.scrollVolume = checked; root.saveConfig(); } }, { label: qsTr("Brightness"), propertyName: "scrollBrightness", onToggled: function (checked) { root.scrollBrightness = checked; root.saveConfig(); } } ] } } } ColumnLayout { id: middleColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Clock") font.pointSize: Appearance.font.size.normal } SwitchRow { label: qsTr("Background") checked: root.clockBackground onToggled: checked => { root.clockBackground = checked; root.saveConfig(); } } SwitchRow { label: qsTr("Show date") checked: root.clockShowDate onToggled: checked => { root.clockShowDate = checked; root.saveConfig(); } } SwitchRow { label: qsTr("Show clock icon") checked: root.clockShowIcon onToggled: checked => { root.clockShowIcon = checked; root.saveConfig(); } } } SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Bar Behavior") font.pointSize: Appearance.font.size.normal } SwitchRow { label: qsTr("Persistent") checked: root.persistent onToggled: checked => { root.persistent = checked; root.saveConfig(); } } SwitchRow { label: qsTr("Show on hover") checked: root.showOnHover onToggled: checked => { root.showOnHover = checked; root.saveConfig(); } } SectionContainer { contentSpacing: Appearance.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Drag threshold") value: root.dragThreshold from: 0 to: 100 suffix: "px" validator: IntValidator { bottom: 0 top: 100 } formatValueFunction: val => Math.round(val).toString() parseValueFunction: text => parseInt(text) onValueModified: newValue => { root.dragThreshold = Math.round(newValue); root.saveConfig(); } } } } SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Active window") font.pointSize: Appearance.font.size.normal } SwitchRow { label: qsTr("Compact") checked: root.activeWindowCompact onToggled: checked => { root.activeWindowCompact = checked; root.saveConfig(); } } SwitchRow { label: qsTr("Inverted") checked: root.activeWindowInverted onToggled: checked => { root.activeWindowInverted = checked; root.saveConfig(); } } } } ColumnLayout { id: rightColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Popouts") font.pointSize: Appearance.font.size.normal } SwitchRow { label: qsTr("Active window") checked: root.popoutActiveWindow onToggled: checked => { root.popoutActiveWindow = checked; root.saveConfig(); } } SwitchRow { label: qsTr("Tray") checked: root.popoutTray onToggled: checked => { root.popoutTray = checked; root.saveConfig(); } } SwitchRow { label: qsTr("Status icons") checked: root.popoutStatusIcons onToggled: checked => { root.popoutStatusIcons = checked; root.saveConfig(); } } } SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Tray Settings") font.pointSize: Appearance.font.size.normal } ConnectedButtonGroup { rootItem: root options: [ { label: qsTr("Background"), propertyName: "trayBackground", onToggled: function (checked) { root.trayBackground = checked; root.saveConfig(); } }, { label: qsTr("Compact"), propertyName: "trayCompact", onToggled: function (checked) { root.trayCompact = checked; root.saveConfig(); } }, { label: qsTr("Recolour"), propertyName: "trayRecolour", onToggled: function (checked) { root.trayRecolour = checked; root.saveConfig(); } } ] } } SectionContainer { Layout.fillWidth: true alignTop: true StyledText { text: qsTr("Monitors") font.pointSize: Appearance.font.size.normal } ConnectedButtonGroup { rootItem: root // max 3 options per line rows: Math.ceil(root.monitorNames.length / 3) options: root.monitorNames.map(e => ({ label: qsTr(e), propertyName: `monitor${e}`, onToggled: function (_) { // if the given monitor is in the excluded list, it should be added back let addedBack = excludedScreens.includes(e); if (addedBack) { const index = excludedScreens.indexOf(e); if (index !== -1) { excludedScreens.splice(index, 1); } } else { if (!excludedScreens.includes(e)) { excludedScreens.push(e); } } root.saveConfig(); }, state: !Strings.testRegexList(root.excludedScreens, e) })) } } } } } } } } ================================================ FILE: modules/dashboard/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface PathArc { relativeX: root.rounding relativeY: root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) } PathLine { relativeX: 0 relativeY: root.wrapper.height - root.roundingY * 2 } PathArc { relativeX: root.rounding relativeY: root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } PathLine { relativeX: root.wrapper.width - root.rounding * 2 relativeY: 0 } PathArc { relativeX: root.rounding relativeY: -root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: -(root.wrapper.height - root.roundingY * 2) } PathArc { relativeX: root.rounding relativeY: -root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) } Behavior on fillColor { CAnim {} } } ================================================ FILE: modules/dashboard/Content.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.filedialog import qs.config import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts Item { id: root required property DrawerVisibilities visibilities readonly property bool needsKeyboard: { const count = repeater.count; for (let i = 0; i < count; i++) { const item = repeater.itemAt(i) as Loader; if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) return true; } return false; } required property DashboardState state required property FileDialog facePicker readonly property var dashboardTabs: { const allTabs = [ { component: dashComponent, iconName: "dashboard", text: qsTr("Dashboard"), enabled: Config.dashboard.showDashboard }, { component: mediaComponent, iconName: "queue_music", text: qsTr("Media"), enabled: Config.dashboard.showMedia }, { component: performanceComponent, iconName: "speed", text: qsTr("Performance"), 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) }, { component: weatherComponent, iconName: "cloud", text: qsTr("Weather"), enabled: Config.dashboard.showWeather } ]; return allTabs.filter(tab => tab.enabled); } readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2 implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight Tabs { id: tabs anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.topMargin: Appearance.padding.normal anchors.margins: Appearance.padding.large nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 state: root.state tabs: root.dashboardTabs } ClippingRectangle { id: viewWrapper anchors.top: tabs.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Appearance.padding.large radius: Appearance.rounding.normal color: "transparent" Flickable { id: view readonly property int currentIndex: root.state.currentTab readonly property Item currentItem: { repeater.count; // Trigger update on count change return repeater.itemAt(currentIndex); } anchors.fill: parent flickableDirection: Flickable.HorizontalFlick implicitWidth: currentItem?.implicitWidth ?? 0 implicitHeight: currentItem?.implicitHeight ?? 0 contentX: currentItem?.x ?? 0 contentWidth: row.implicitWidth contentHeight: row.implicitHeight onContentXChanged: { if (!moving || !currentItem) return; const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 2) root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 2) root.state.currentTab = Math.max(root.state.currentTab - 1, 0); } onDragEnded: { if (!currentItem) return; const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 10) root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 10) root.state.currentTab = Math.max(root.state.currentTab - 1, 0); else contentX = Qt.binding(() => currentItem?.x ?? 0); } RowLayout { id: row Repeater { id: repeater model: ScriptModel { values: root.dashboardTabs } delegate: Loader { id: paneLoader required property int index required property var modelData Layout.alignment: Qt.AlignTop sourceComponent: modelData.component Component.onCompleted: active = Qt.binding(() => { if (index === view.currentIndex) return true; const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); }) } } } Component { id: dashComponent Dash { visibilities: root.visibilities state: root.state facePicker: root.facePicker } } Component { id: mediaComponent MediaWrapper { visibilities: root.visibilities } } Component { id: performanceComponent Performance {} } Component { id: weatherComponent Weather {} } Behavior on contentX { Anim {} } } } Behavior on implicitWidth { Anim { duration: Appearance.anim.durations.large easing.bezierCurve: Appearance.anim.curves.emphasized } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.large easing.bezierCurve: Appearance.anim.curves.emphasized } } } ================================================ FILE: modules/dashboard/Dash.qml ================================================ import qs.components import qs.components.filedialog import qs.services import qs.config import "dash" import QtQuick.Layouts GridLayout { id: root required property DrawerVisibilities visibilities required property DashboardState state required property FileDialog facePicker rowSpacing: Appearance.spacing.normal columnSpacing: Appearance.spacing.normal Rect { Layout.column: 2 Layout.columnSpan: 3 Layout.preferredWidth: user.implicitWidth Layout.preferredHeight: user.implicitHeight radius: Appearance.rounding.large User { id: user visibilities: root.visibilities state: root.state facePicker: root.facePicker } } Rect { Layout.row: 0 Layout.columnSpan: 2 Layout.preferredWidth: Config.dashboard.sizes.weatherWidth Layout.fillHeight: true radius: Appearance.rounding.large * 1.5 Weather {} } Rect { Layout.row: 1 Layout.preferredWidth: dateTime.implicitWidth Layout.fillHeight: true radius: Appearance.rounding.normal DateTime { id: dateTime } } Rect { Layout.row: 1 Layout.column: 1 Layout.columnSpan: 3 Layout.fillWidth: true Layout.preferredHeight: calendar.implicitHeight radius: Appearance.rounding.large Calendar { id: calendar state: root.state } } Rect { Layout.row: 1 Layout.column: 4 Layout.preferredWidth: resources.implicitWidth Layout.fillHeight: true radius: Appearance.rounding.normal Resources { id: resources } } Rect { Layout.row: 0 Layout.column: 5 Layout.rowSpan: 2 Layout.preferredWidth: media.implicitWidth Layout.fillHeight: true radius: Appearance.rounding.large * 2 Media { id: media } } component Rect: StyledRect { color: Colours.tPalette.m3surfaceContainer } } ================================================ FILE: modules/dashboard/LyricMenu.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property real contentHeight function searchCandidates(title, artist) { LyricsService.currentRequestId++; LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); } implicitHeight: contentHeight radius: Appearance.rounding.large color: Colours.tPalette.m3surfaceContainer Loader { asynchronous: true anchors.fill: parent active: root.height > 0 sourceComponent: ColumnLayout { anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal // Header: icon, backend name, refresh, toggle RowLayout { Layout.fillWidth: true spacing: Appearance.padding.small MaterialIcon { text: "lyrics" fill: 1 color: Colours.palette.m3primary font.pointSize: Appearance.spacing.large } StyledText { Layout.fillWidth: true text: LyricsService.backend font.pointSize: Appearance.font.size.normal color: Colours.palette.m3secondary elide: Text.ElideRight } IconButton { icon: "refresh" type: IconButton.Text onClicked: LyricsService.loadLyrics() } StyledSwitch { checked: LyricsService.lyricsVisible onToggled: LyricsService.toggleVisibility() } } StyledText { Layout.fillWidth: true text: "Fetched Candidates:" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small elide: Text.ElideRight } // Candidates list ListView { id: candidatesView Layout.fillWidth: true Layout.fillHeight: true visible: LyricsService.candidatesModel.count > 0 model: LyricsService.candidatesModel clip: true spacing: Appearance.spacing.small opacity: visible ? 1 : 0 // Behavior on opacity { // NumberAnimation { duration: Appearance.anim.durations.normal } // } delegate: Item { id: delegateRoot required property real id required property string title required property string artist property bool hovered: false property bool pressed: false width: ListView.view.width * 0.98 height: 70 anchors.horizontalCenter: parent?.horizontalCenter scale: hovered ? 1.02 : 1.0 Behavior on scale { NumberAnimation { duration: Appearance.anim.durations.small easing.type: Easing.OutCubic } } Rectangle { id: background anchors.fill: parent radius: Appearance.rounding.small 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) border.width: delegateRoot.hovered ? 1 : 0 border.color: Colours.palette.m3primary Behavior on color { ColorAnimation { duration: Appearance.anim.durations.small } } Behavior on border.width { NumberAnimation { duration: Appearance.anim.durations.small } } } MouseArea { anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onEntered: delegateRoot.hovered = true onExited: delegateRoot.hovered = false onPressed: delegateRoot.pressed = true onReleased: delegateRoot.pressed = false onClicked: LyricsService.selectCandidate(delegateRoot.id) } Row { anchors.fill: parent anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.small // Active indicator bar Rectangle { width: 4 height: parent.height * 0.6 radius: 2 anchors.verticalCenter: parent.verticalCenter color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" Behavior on color { ColorAnimation { duration: Appearance.anim.durations.small } } } Column { anchors.verticalCenter: parent.verticalCenter width: parent.width - 30 spacing: 4 Text { text: delegateRoot.title font.pointSize: Appearance.font.size.normal font.bold: true color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface width: parent.width elide: Text.ElideRight Behavior on color { ColorAnimation { duration: Appearance.anim.durations.small } } } Text { text: delegateRoot.artist font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant elide: Text.ElideRight } } } } } Item { Layout.fillHeight: true visible: LyricsService.candidatesModel.count == 0 } // Manual search ColumnLayout { Layout.fillWidth: true spacing: Appearance.padding.small StyledText { Layout.fillWidth: true text: "Manual Search" font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant elide: Text.ElideRight } RowLayout { Layout.fillWidth: true spacing: Appearance.padding.small StyledInputField { id: searchTitle Layout.fillWidth: true horizontalAlignment: TextInput.AlignLeft Binding { target: searchTitle property: "text" value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") } } StyledInputField { id: searchArtist Layout.fillWidth: true horizontalAlignment: TextInput.AlignLeft Binding { target: searchArtist property: "text" value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") } } IconButton { icon: "search" onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) } } } // Offset controls RowLayout { Layout.fillWidth: true spacing: Appearance.padding.small MaterialIcon { text: "contrast_square" font.pointSize: Appearance.font.size.large color: Colours.palette.m3secondary } StyledText { text: "Offset" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal } Item { Layout.fillWidth: true } IconButton { icon: "remove" type: IconButton.Text onClicked: { LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); LyricsService.savePrefs(); } } TextInput { id: offsetInput horizontalAlignment: TextInput.AlignHCenter color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.normal selectByMouse: true text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" onEditingFinished: { let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); let val = parseFloat(cleaned); if (!isNaN(val)) { LyricsService.offset = parseFloat(val.toFixed(1)); LyricsService.savePrefs(); } else { offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; } } Binding { target: offsetInput property: "text" value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" when: !offsetInput.activeFocus } Connections { function onCurrentRequestIdChanged() { offsetInput.focus = false; } target: LyricsService } } IconButton { icon: "add" type: IconButton.Text onClicked: { LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); LyricsService.savePrefs(); } } } } } } ================================================ FILE: modules/dashboard/LyricsView.qml ================================================ import qs.components import qs.components.containers import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Effects StyledListView { id: root readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 clip: true model: LyricsService.model currentIndex: LyricsService.currentIndex visible: lyricsActuallyVisible || hideTimer.running preferredHighlightBegin: height / 2 - 30 preferredHighlightEnd: height / 2 + 30 highlightRangeMode: ListView.ApplyRange highlightFollowsCurrentItem: true highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Appearance.anim.durations.normal layer.enabled: true layer.effect: ShaderEffect { required property Item source property real fadeMargin: 0.5 fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") } onLyricsActuallyVisibleChanged: { if (!lyricsActuallyVisible) hideTimer.restart(); } onModelChanged: { if (model && model.count > 0) { Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); } } delegate: Item { id: delegateRoot required property string lyricLine required property real time required property int index readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 property bool isCurrent: ListView.isCurrentItem width: ListView.view.width height: hasContent ? (lyricText.contentHeight + Appearance.spacing.large) : 0 MultiEffect { id: effect anchors.fill: lyricText source: lyricText scale: lyricText.scale enabled: delegateRoot.isCurrent visible: delegateRoot.isCurrent blurEnabled: true blur: 0.4 shadowEnabled: true shadowColor: Colours.palette.m3primary shadowOpacity: 0.5 shadowBlur: 0.6 shadowHorizontalOffset: 0 shadowVerticalOffset: 0 autoPaddingEnabled: true } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) } Text { id: lyricText text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" width: parent.width * 0.85 anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap font.pointSize: Appearance.font.size.normal color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant font.bold: delegateRoot.isCurrent scale: delegateRoot.isCurrent ? 1.15 : 1.0 Behavior on color { CAnim { duration: Appearance.anim.durations.small } } Behavior on scale { Anim { duration: Appearance.anim.durations.small } } } } Timer { id: hideTimer interval: 300 // long enough to bridge the track switch gap running: false repeat: false } } ================================================ FILE: modules/dashboard/Media.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.utils import qs.config import Caelestia.Services import Quickshell import Quickshell.Services.Mpris import QtQuick import QtQuick.Layouts import QtQuick.Shapes Item { id: root required property DrawerVisibilities visibilities readonly property bool needsKeyboard: lyricMenuOpen readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight property bool lyricMenuOpen: false property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 property bool lyricsShowingDebounced: false property real playerProgress: { const active = Players.active; return active?.length ? active.position / active.length : 0; } function lengthStr(length: int): string { if (length < 0) return "-1:-1"; const hours = Math.floor(length / 3600); const mins = Math.floor((length % 3600) / 60); const secs = Math.floor(length % 60).toString().padStart(2, "0"); if (hours > 0) return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; return `${mins}:${secs}`; } onLyricsShowingChanged: { if (lyricsShowing) { lyricsHideDelay.stop(); lyricsShowingDebounced = true; } else { lyricsHideDelay.restart(); } } implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 implicitHeight: nonAnimHeight Behavior on implicitHeight { Anim {} } Behavior on playerProgress { Anim { duration: Appearance.anim.durations.large } } Timer { running: Players.active?.isPlaying ?? false interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: { if (!Players.active) return; LyricsService.updatePosition(); Players.active?.positionChanged(); } } Timer { id: lyricsHideDelay interval: 300 repeat: false } Connections { function onTriggered() { root.lyricsShowingDebounced = false; } target: lyricsHideDelay } ServiceRef { service: Audio.cava } ServiceRef { service: Audio.beatTracker } Shape { id: visualiser readonly property real centerX: width / 2 readonly property real centerY: height / 2 readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small property color colour: Colours.palette.m3primary anchors.fill: cover anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize asynchronous: true preferredRendererType: Shape.CurveRenderer data: visualiserBars.instances } Variants { id: visualiserBars model: Array.from({ length: Config.services.visualiserBars }, (_, i) => i) ShapePath { id: visualiserBar required property int modelData readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize readonly property real cos: Math.cos(angle) readonly property real sin: Math.sin(angle) capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4 strokeColor: Colours.palette.m3primary startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos startY: visualiser.centerY + (visualiser.innerY + strokeWidth / 2) * sin PathLine { x: visualiser.centerX + (visualiser.innerX + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.cos y: visualiser.centerY + (visualiser.innerY + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.sin } Behavior on strokeColor { CAnim {} } } } StyledClippingRect { id: cover anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize implicitWidth: Config.dashboard.sizes.mediaCoverArtSize implicitHeight: Config.dashboard.sizes.mediaCoverArtSize color: Colours.tPalette.m3surfaceContainerHigh radius: Infinity MaterialIcon { anchors.centerIn: parent grade: 200 text: "art_track" color: Colours.palette.m3onSurfaceVariant font.pointSize: (parent.width * 0.4) || 1 } Image { id: image anchors.fill: parent source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height MouseArea { anchors.fill: parent onClicked: { LyricsService.toggleVisibility(); } } } } ColumnLayout { id: details anchors.verticalCenter: parent.verticalCenter anchors.left: visualiser.right anchors.leftMargin: Appearance.spacing.normal spacing: Appearance.spacing.small StyledText { id: title Layout.fillWidth: true Layout.maximumWidth: parent.implicitWidth animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } StyledText { id: album Layout.fillWidth: true Layout.maximumWidth: parent.implicitWidth animate: true horizontalAlignment: Text.AlignHCenter visible: !!Players.active text: Players.active?.trackAlbum || qsTr("Unknown album") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small elide: Text.ElideRight } StyledText { id: artist Layout.fillWidth: true Layout.maximumWidth: parent.implicitWidth animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackArtist ?? qsTr("Play some music for stuff to show up here!")) || qsTr("Unknown artist") color: Players.active ? Colours.palette.m3secondary : Colours.palette.m3outline elide: Text.ElideRight wrapMode: Players.active ? Text.NoWrap : Text.WordWrap } LyricsView { id: lyricsViewInDetails Layout.fillWidth: true Layout.preferredHeight: 200 } RowLayout { id: controls Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small Layout.bottomMargin: Appearance.spacing.smaller spacing: Appearance.spacing.small PlayerControl { type: IconButton.Text icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" font.pointSize: Math.round(Appearance.font.size.large) disabled: !Players.active?.shuffleSupported onClicked: Players.active.shuffle = !Players.active?.shuffle } PlayerControl { type: IconButton.Text icon: "skip_previous" font.pointSize: Math.round(Appearance.font.size.large * 1.5) disabled: !Players.active?.canGoPrevious onClicked: Players.active?.previous() } PlayerControl { icon: Players.active?.isPlaying ? "pause" : "play_arrow" label.animate: true toggle: true padding: Appearance.padding.small / 2 checked: Players.active?.isPlaying ?? false font.pointSize: Math.round(Appearance.font.size.large * 1.5) disabled: !Players.active?.canTogglePlaying onClicked: Players.active?.togglePlaying() } PlayerControl { type: IconButton.Text icon: "skip_next" font.pointSize: Math.round(Appearance.font.size.large * 1.5) disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } PlayerControl { type: IconButton.Text icon: "lyrics" font.pointSize: Math.round(Appearance.font.size.large) onClicked: root.lyricMenuOpen = !root.lyricMenuOpen } } StyledSlider { id: slider enabled: !!Players.active implicitWidth: 280 implicitHeight: Appearance.padding.normal * 3 onMoved: { const active = Players.active; if (active?.canSeek && active?.positionSupported) active.position = value * active.length; } Binding { target: slider property: "value" value: root.playerProgress when: !slider.pressed } CustomMouseArea { function onWheel(event: WheelEvent) { const active = Players.active; if (!active?.canSeek || !active?.positionSupported) return; event.accepted = true; const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds Qt.callLater(() => { active.position = Math.max(0, Math.min(active.length, active.position + delta)); }); } anchors.fill: parent acceptedButtons: Qt.NoButton } } Item { Layout.fillWidth: true implicitHeight: Math.max(position.implicitHeight, length.implicitHeight) StyledText { id: position anchors.left: parent.left text: root.lengthStr(Players.active?.position ?? -1) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } StyledText { id: length anchors.right: parent.right text: root.lengthStr(Players.active?.length ?? -1) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } } } ColumnLayout { id: leftSection anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 anchors.left: details.right anchors.leftMargin: Appearance.spacing.normal visible: lyricMenu.height === 0 || opacity > 0 opacity: lyricMenu.height === 0 ? 1 : 0 Behavior on opacity { NumberAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.OutCubic } } Item { id: bongocat implicitWidth: visualiser.width implicitHeight: visualiser.height AnimatedImage { anchors.centerIn: parent width: visualiser.width * 0.75 height: visualiser.height * 0.75 playing: Players.active?.isPlaying ?? false speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit } } } LyricMenu { id: lyricMenu anchors.top: parent.top anchors.left: details.right anchors.right: parent.right anchors.leftMargin: Appearance.spacing.normal contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Appearance.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight visible: root.lyricMenuOpen || height > 0 height: root.lyricMenuOpen ? implicitHeight : 0 clip: true Behavior on height { NumberAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.OutCubic } } } RowLayout { id: playerChanger parent: !root.lyricsShowingDebounced ? details : leftSection Layout.alignment: Qt.AlignHCenter spacing: Appearance.spacing.small PlayerControl { type: IconButton.Text icon: "move_up" inactiveOnColour: Colours.palette.m3secondary padding: Appearance.padding.small font.pointSize: Appearance.font.size.large disabled: !Players.active?.canRaise onClicked: { Players.active?.raise(); root.visibilities.dashboard = false; } } SplitButton { id: playerSelector disabled: !Players.list.length active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData menuItems: playerList.instances fallbackIcon: "music_off" fallbackText: qsTr("No players") label.Layout.maximumWidth: slider.implicitWidth * 0.28 label.elide: Text.ElideRight stateLayer.disabled: true menuOnTop: true Variants { id: playerList model: Players.list PlayerItem {} } } PlayerControl { type: IconButton.Text icon: "delete" inactiveOnColour: Colours.palette.m3error padding: Appearance.padding.small font.pointSize: Appearance.font.size.large disabled: !Players.active?.canQuit onClicked: Players.active?.quit() } } component PlayerItem: MenuItem { required property MprisPlayer modelData icon: modelData === Players.active ? "check" : "" text: Players.getIdentity(modelData) activeIcon: "animated_images" } component PlayerControl: IconButton { Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2 radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } } ================================================ FILE: modules/dashboard/MediaWrapper.qml ================================================ import QtQuick Item { property alias visibilities: media.visibilities readonly property alias needsKeyboard: media.needsKeyboard implicitWidth: media.implicitWidth implicitHeight: media.nonAnimHeight Media { id: media } } ================================================ FILE: modules/dashboard/Performance.qml ================================================ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.UPower import Caelestia.Internal import qs.components import qs.components.misc import qs.config import qs.services Item { id: root readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2 function displayTemp(temp: real): string { return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? "F" : "C"}`; } implicitWidth: Math.max(minWidth, content.implicitWidth) implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight StyledRect { id: placeholder anchors.centerIn: parent width: 400 height: 350 radius: Appearance.rounding.large color: Colours.tPalette.m3surfaceContainer 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) ColumnLayout { anchors.centerIn: parent spacing: Appearance.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "tune" font.pointSize: Appearance.font.size.extraLarge * 2 color: Colours.palette.m3onSurfaceVariant } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No widgets enabled") font.pointSize: Appearance.font.size.large color: Colours.palette.m3onSurface } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enable widgets in dashboard settings") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } } } RowLayout { id: content anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.normal visible: !placeholder.visible Ref { service: SystemUsage } ColumnLayout { id: mainColumn Layout.fillWidth: true spacing: Appearance.spacing.normal RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") HeroCard { Layout.fillWidth: true Layout.minimumWidth: 400 Layout.preferredHeight: 150 visible: Config.dashboard.performance.showCpu icon: "memory" title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU") mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%` mainLabel: qsTr("Usage") secondaryValue: root.displayTemp(SystemUsage.cpuTemp) secondaryLabel: qsTr("Temp") usage: SystemUsage.cpuPerc temperature: SystemUsage.cpuTemp accentColor: Colours.palette.m3primary } HeroCard { Layout.fillWidth: true Layout.minimumWidth: 400 Layout.preferredHeight: 150 visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE" icon: "desktop_windows" title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU") mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%` mainLabel: qsTr("Usage") secondaryValue: root.displayTemp(SystemUsage.gpuTemp) secondaryLabel: qsTr("Temp") usage: SystemUsage.gpuPerc temperature: SystemUsage.gpuTemp accentColor: Colours.palette.m3secondary } } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork GaugeCard { Layout.minimumWidth: 250 Layout.preferredHeight: 220 Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork icon: "memory_alt" title: qsTr("Memory") percentage: SystemUsage.memPerc subtitle: { const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed); const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal); return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; } accentColor: Colours.palette.m3tertiary visible: Config.dashboard.performance.showMemory } StorageGaugeCard { Layout.minimumWidth: 250 Layout.preferredHeight: 220 Layout.fillWidth: !Config.dashboard.performance.showNetwork visible: Config.dashboard.performance.showStorage } NetworkCard { Layout.fillWidth: true Layout.minimumWidth: 200 Layout.preferredHeight: 220 visible: Config.dashboard.performance.showNetwork } } } BatteryTank { Layout.preferredWidth: 120 Layout.preferredHeight: mainColumn.implicitHeight visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery } } component BatteryTank: StyledClippingRect { id: batteryTank property real percentage: UPower.displayDevice.percentage property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging property color accentColor: Colours.palette.m3primary property real animatedPercentage: 0 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.large Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage // Background Fill StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: parent.height * batteryTank.animatedPercentage color: Qt.alpha(batteryTank.accentColor, 0.15) } ColumnLayout { anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.small // Header Section ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small MaterialIcon { text: { if (!UPower.displayDevice.isLaptopBattery) { if (PowerProfiles.profile === PowerProfile.PowerSaver) return "energy_savings_leaf"; if (PowerProfiles.profile === PowerProfile.Performance) return "rocket_launch"; return "balance"; } if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) return "battery_full"; const perc = UPower.displayDevice.percentage; const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); if (perc >= 0.99) return "battery_full"; let level = Math.floor(perc * 7); if (charging && (level === 4 || level === 1)) level--; return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } font.pointSize: Appearance.font.size.large color: batteryTank.accentColor } StyledText { Layout.fillWidth: true text: qsTr("Battery") font.pointSize: Appearance.font.size.normal color: Colours.palette.m3onSurface } } Item { Layout.fillHeight: true } // Bottom Info Section ColumnLayout { Layout.fillWidth: true spacing: -4 StyledText { Layout.alignment: Qt.AlignRight text: `${Math.round(batteryTank.percentage * 100)}%` font.pointSize: Appearance.font.size.extraLarge font.weight: Font.Medium color: batteryTank.accentColor } StyledText { Layout.alignment: Qt.AlignRight text: { if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) return qsTr("Full"); if (batteryTank.isCharging) return qsTr("Charging"); const s = UPower.displayDevice.timeToEmpty; if (s === 0) return qsTr("..."); const hr = Math.floor(s / 3600); const min = Math.floor((s % 3600) / 60); if (hr > 0) return `${hr}h ${min}m`; return `${min}m`; } font.pointSize: Appearance.font.size.smaller color: Colours.palette.m3onSurfaceVariant } } } Behavior on animatedPercentage { Anim { duration: Appearance.anim.durations.large } } } component CardHeader: RowLayout { property string icon property string title property color accentColor: Colours.palette.m3primary Layout.fillWidth: true spacing: Appearance.spacing.small MaterialIcon { text: parent.icon fill: 1 color: parent.accentColor font.pointSize: Appearance.spacing.large } StyledText { Layout.fillWidth: true text: parent.title font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } } component ProgressBar: StyledRect { id: progressBar property real value: 0 property color fgColor: Colours.palette.m3primary property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) property real animatedValue: 0 color: bgColor radius: Appearance.rounding.full Component.onCompleted: animatedValue = value onValueChanged: animatedValue = value StyledRect { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: parent.width * progressBar.animatedValue color: progressBar.fgColor radius: Appearance.rounding.full } Behavior on animatedValue { Anim { duration: Appearance.anim.durations.large } } } component HeroCard: StyledClippingRect { id: heroCard property string icon property string title property string mainValue property string mainLabel property string secondaryValue property string secondaryLabel property real usage: 0 property real temperature: 0 property color accentColor: Colours.palette.m3primary readonly property real maxTemp: 100 readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp)) property real animatedUsage: 0 property real animatedTemp: 0 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.large Component.onCompleted: { animatedUsage = usage; animatedTemp = tempProgress; } onUsageChanged: animatedUsage = usage onTempProgressChanged: animatedTemp = tempProgress StyledRect { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: parent.width * heroCard.animatedUsage color: Qt.alpha(heroCard.accentColor, 0.15) } ColumnLayout { anchors.fill: parent anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large anchors.topMargin: Appearance.padding.normal anchors.bottomMargin: Appearance.padding.normal spacing: Appearance.spacing.small CardHeader { icon: heroCard.icon title: heroCard.title accentColor: heroCard.accentColor } RowLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: Appearance.spacing.normal Column { Layout.alignment: Qt.AlignBottom Layout.fillWidth: true spacing: Appearance.spacing.small Row { spacing: Appearance.spacing.small StyledText { text: heroCard.secondaryValue font.pointSize: Appearance.font.size.normal font.weight: Font.Medium } StyledText { text: heroCard.secondaryLabel font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant anchors.baseline: parent.children[0].baseline } } ProgressBar { width: parent.width * 0.5 height: 6 value: heroCard.tempProgress fgColor: heroCard.accentColor bgColor: Qt.alpha(heroCard.accentColor, 0.2) } } Item { Layout.fillWidth: true } } } Column { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large anchors.rightMargin: 32 spacing: 0 StyledText { anchors.right: parent.right text: heroCard.mainLabel font.pointSize: Appearance.font.size.normal color: Colours.palette.m3onSurfaceVariant } StyledText { anchors.right: parent.right text: heroCard.mainValue font.pointSize: Appearance.font.size.extraLarge font.weight: Font.Medium color: heroCard.accentColor } } Behavior on animatedUsage { Anim { duration: Appearance.anim.durations.large } } Behavior on animatedTemp { Anim { duration: Appearance.anim.durations.large } } } component GaugeCard: StyledRect { id: gaugeCard property string icon property string title property real percentage: 0 property string subtitle property color accentColor: Colours.palette.m3primary readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcSweep: 1.5 * Math.PI property real animatedPercentage: 0 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.large clip: true Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage ColumnLayout { anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.smaller CardHeader { icon: gaugeCard.icon title: gaugeCard.title accentColor: gaugeCard.accentColor } Item { Layout.fillWidth: true Layout.fillHeight: true ArcGauge { anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width percentage: gaugeCard.animatedPercentage accentColor: gaugeCard.accentColor trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) startAngle: gaugeCard.arcStartAngle sweepAngle: gaugeCard.arcSweep } StyledText { anchors.centerIn: parent text: `${Math.round(gaugeCard.percentage * 100)}%` font.pointSize: Appearance.font.size.extraLarge font.weight: Font.Medium color: gaugeCard.accentColor } } StyledText { Layout.alignment: Qt.AlignHCenter text: gaugeCard.subtitle font.pointSize: Appearance.font.size.smaller color: Colours.palette.m3onSurfaceVariant } } Behavior on animatedPercentage { Anim { duration: Appearance.anim.durations.large } } } component StorageGaugeCard: StyledRect { id: storageGaugeCard property int currentDiskIndex: 0 readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null property int diskCount: 0 readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcSweep: 1.5 * Math.PI property real animatedPercentage: 0 property color accentColor: Colours.palette.m3secondary color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.large clip: true Component.onCompleted: { diskCount = SystemUsage.disks.length; if (currentDisk) animatedPercentage = currentDisk.perc; } onCurrentDiskChanged: { if (currentDisk) animatedPercentage = currentDisk.perc; } // Update diskCount and animatedPercentage when disks data changes Connections { function onDisksChanged() { if (SystemUsage.disks.length !== storageGaugeCard.diskCount) storageGaugeCard.diskCount = SystemUsage.disks.length; // Update animated percentage when disk data refreshes if (storageGaugeCard.currentDisk) storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; } target: SystemUsage } MouseArea { anchors.fill: parent onWheel: wheel => { if (wheel.angleDelta.y > 0) storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; else if (wheel.angleDelta.y < 0) storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; } } ColumnLayout { anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.smaller CardHeader { icon: "hard_disk" title: { const base = qsTr("Storage"); if (!storageGaugeCard.currentDisk) return base; return `${base} - ${storageGaugeCard.currentDisk.mount}`; } accentColor: storageGaugeCard.accentColor // Scroll hint icon MaterialIcon { text: "unfold_more" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.normal visible: storageGaugeCard.diskCount > 1 opacity: 0.7 ToolTip.visible: hintHover.hovered ToolTip.text: qsTr("Scroll to switch disks") ToolTip.delay: 500 HoverHandler { id: hintHover } } } Item { Layout.fillWidth: true Layout.fillHeight: true ArcGauge { anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width percentage: storageGaugeCard.animatedPercentage accentColor: storageGaugeCard.accentColor trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) startAngle: storageGaugeCard.arcStartAngle sweepAngle: storageGaugeCard.arcSweep } StyledText { anchors.centerIn: parent text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" font.pointSize: Appearance.font.size.extraLarge font.weight: Font.Medium color: storageGaugeCard.accentColor } } StyledText { Layout.alignment: Qt.AlignHCenter text: { if (!storageGaugeCard.currentDisk) return "—"; const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; } font.pointSize: Appearance.font.size.smaller color: Colours.palette.m3onSurfaceVariant } } Behavior on animatedPercentage { Anim { duration: Appearance.anim.durations.large } } } component NetworkCard: StyledRect { id: networkCard property color accentColor: Colours.palette.m3primary color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.large clip: true Ref { service: NetworkUsage } ColumnLayout { anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.small CardHeader { icon: "swap_vert" title: qsTr("Network") accentColor: networkCard.accentColor } // Sparkline graph Item { Layout.fillWidth: true Layout.fillHeight: true SparklineItem { id: sparkline property real targetMax: 1024 property real smoothMax: targetMax anchors.fill: parent line1: NetworkUsage.uploadBuffer line1Color: Colours.palette.m3secondary line1FillAlpha: 0.15 line2: NetworkUsage.downloadBuffer line2Color: Colours.palette.m3tertiary line2FillAlpha: 0.2 maxValue: smoothMax historyLength: NetworkUsage.historyLength Connections { function onValuesChanged(): void { sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); slideAnim.restart(); } target: NetworkUsage.downloadBuffer } NumberAnimation { id: slideAnim target: sparkline property: "slideProgress" from: 0 to: 1 duration: Config.dashboard.resourceUpdateInterval } Behavior on smoothMax { Anim { duration: Appearance.anim.durations.large } } } // "No data" placeholder StyledText { anchors.centerIn: parent text: qsTr("Collecting data...") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant visible: NetworkUsage.downloadBuffer.count < 2 opacity: 0.6 } } // Download row RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal MaterialIcon { text: "download" color: Colours.palette.m3tertiary font.pointSize: Appearance.font.size.normal } StyledText { text: qsTr("Download") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } Item { Layout.fillWidth: true } StyledText { text: { const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; } font.pointSize: Appearance.font.size.normal font.weight: Font.Medium color: Colours.palette.m3tertiary } } // Upload row RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal MaterialIcon { text: "upload" color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.normal } StyledText { text: qsTr("Upload") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } Item { Layout.fillWidth: true } StyledText { text: { const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; } font.pointSize: Appearance.font.size.normal font.weight: Font.Medium color: Colours.palette.m3secondary } } // Session totals RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal MaterialIcon { text: "history" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.normal } StyledText { text: qsTr("Total") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } Item { Layout.fillWidth: true } StyledText { text: { const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; } font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } } } } } ================================================ FILE: modules/dashboard/Tabs.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Controls Item { id: root required property real nonAnimWidth required property DashboardState state required property var tabs readonly property alias count: bar.count implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight TabBar { id: bar anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top currentIndex: root.state.currentTab background: null onCurrentIndexChanged: root.state.currentTab = currentIndex Repeater { model: ScriptModel { values: root.tabs } delegate: Tab { required property var modelData iconName: modelData.iconName text: modelData.text } } } Item { id: indicator anchors.top: bar.bottom anchors.topMargin: 5 implicitWidth: { const tab = bar.currentItem; if (tab) return tab.implicitWidth; const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; return width; } implicitHeight: 3 x: { const tab = bar.currentItem; const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; const tabWidth = tab?.implicitWidth ?? width; return width * bar.currentIndex + (width - tabWidth) / 2; } clip: true StyledRect { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right implicitHeight: parent.implicitHeight * 2 color: Colours.palette.m3primary radius: Appearance.rounding.full } Behavior on x { Anim {} } Behavior on implicitWidth { Anim {} } } StyledRect { id: separator anchors.top: indicator.bottom anchors.left: parent.left anchors.right: parent.right implicitHeight: 1 color: Colours.palette.m3outlineVariant } component Tab: TabButton { id: tab required property string iconName readonly property bool current: TabBar.tabBar.currentItem === this background: null contentItem: CustomMouseArea { id: mouse function onWheel(event: WheelEvent): void { if (event.angleDelta.y < 0) root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); else if (event.angleDelta.y > 0) root.state.currentTab = Math.max(root.state.currentTab - 1, 0); } implicitWidth: Math.max(icon.width, label.width) implicitHeight: icon.height + label.height cursorShape: Qt.PointingHandCursor onPressed: event => { root.state.currentTab = tab.TabBar.index; const stateY = stateWrapper.y; rippleAnim.x = event.x; rippleAnim.y = event.y - stateY; const dist = (ox, oy) => ox * ox + oy * oy; 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))); rippleAnim.restart(); } SequentialAnimation { id: rippleAnim property real x property real y property real radius PropertyAction { target: ripple property: "x" value: rippleAnim.x } PropertyAction { target: ripple property: "y" value: rippleAnim.y } PropertyAction { target: ripple property: "opacity" value: 0.08 } Anim { target: ripple properties: "implicitWidth,implicitHeight" from: 0 to: rippleAnim.radius * 2 duration: Appearance.anim.durations.normal easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { target: ripple property: "opacity" to: 0 duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } } ClippingRectangle { id: stateWrapper anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2 color: "transparent" radius: Appearance.rounding.small StyledRect { id: stateLayer anchors.fill: parent color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 Behavior on opacity { Anim {} } } StyledRect { id: ripple radius: Appearance.rounding.full color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface opacity: 0 transform: Translate { x: -ripple.width / 2 y: -ripple.height / 2 } } } MaterialIcon { id: icon anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: label.top text: tab.iconName color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant fill: tab.current ? 1 : 0 font.pointSize: Appearance.font.size.large Behavior on fill { Anim {} } } StyledText { id: label anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom text: tab.text color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant } } } } ================================================ FILE: modules/dashboard/Weather.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts Item { id: root readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 implicitHeight: layout.implicitHeight Component.onCompleted: Weather.reload() ColumnLayout { id: layout anchors.fill: parent spacing: Appearance.spacing.smaller RowLayout { Layout.leftMargin: Appearance.padding.large Layout.rightMargin: Appearance.padding.large Layout.fillWidth: true Column { spacing: Appearance.spacing.small / 2 StyledText { text: Weather.city || qsTr("Loading...") font.pointSize: Appearance.font.size.extraLarge font.weight: 600 color: Colours.palette.m3onSurface } StyledText { text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant } } Item { Layout.fillWidth: true } Row { spacing: Appearance.spacing.large WeatherStat { icon: "wb_twilight" label: "Sunrise" value: Weather.sunrise colour: Colours.palette.m3tertiary } WeatherStat { icon: "bedtime" label: "Sunset" value: Weather.sunset colour: Colours.palette.m3tertiary } } } StyledRect { Layout.fillWidth: true implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.large * 2 color: Colours.tPalette.m3surfaceContainer RowLayout { id: bigInfoRow anchors.centerIn: parent spacing: Appearance.spacing.large MaterialIcon { Layout.alignment: Qt.AlignVCenter text: Weather.icon font.pointSize: Appearance.font.size.extraLarge * 3 color: Colours.palette.m3secondary animate: true } ColumnLayout { Layout.alignment: Qt.AlignVCenter spacing: -Appearance.spacing.small StyledText { text: Weather.temp font.pointSize: Appearance.font.size.extraLarge * 2 font.weight: 500 color: Colours.palette.m3primary } StyledText { Layout.leftMargin: Appearance.padding.small text: Weather.description font.pointSize: Appearance.font.size.normal color: Colours.palette.m3onSurfaceVariant } } } } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller DetailCard { icon: "water_drop" label: "Humidity" value: Weather.humidity + "%" colour: Colours.palette.m3secondary } DetailCard { icon: "thermostat" label: "Feels Like" value: Weather.feelsLike colour: Colours.palette.m3primary } DetailCard { icon: "air" label: "Wind" value: Weather.windSpeed ? Weather.windSpeed + " km/h" : "--" colour: Colours.palette.m3tertiary } } StyledText { Layout.topMargin: Appearance.spacing.normal Layout.leftMargin: Appearance.padding.normal visible: forecastRepeater.count > 0 text: qsTr("7-Day Forecast") font.pointSize: Appearance.font.size.normal font.weight: 600 color: Colours.palette.m3onSurface } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.smaller Repeater { id: forecastRepeater model: Weather.forecast StyledRect { id: forecastItem required property int index required property var modelData Layout.fillWidth: true implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: forecastItemColumn anchors.centerIn: parent spacing: Appearance.spacing.small StyledText { Layout.alignment: Qt.AlignHCenter text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") font.pointSize: Appearance.font.size.normal font.weight: 600 color: Colours.palette.m3primary } StyledText { Layout.topMargin: -Appearance.spacing.small / 2 Layout.alignment: Qt.AlignHCenter text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") font.pointSize: Appearance.font.size.small opacity: 0.7 color: Colours.palette.m3onSurfaceVariant } MaterialIcon { Layout.alignment: Qt.AlignHCenter text: forecastItem.modelData.icon font.pointSize: Appearance.font.size.extraLarge color: Colours.palette.m3secondary } StyledText { Layout.alignment: Qt.AlignHCenter text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" font.weight: 600 color: Colours.palette.m3tertiary } } } } } } component DetailCard: StyledRect { id: detailRoot property string icon property string label property string value property color colour Layout.fillWidth: true Layout.preferredHeight: 60 radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainer Row { anchors.centerIn: parent spacing: Appearance.spacing.normal MaterialIcon { text: detailRoot.icon color: detailRoot.colour font.pointSize: Appearance.font.size.large anchors.verticalCenter: parent.verticalCenter } Column { anchors.verticalCenter: parent.verticalCenter spacing: 0 StyledText { text: detailRoot.label font.pointSize: Appearance.font.size.smaller opacity: 0.7 horizontalAlignment: Text.AlignLeft } StyledText { text: detailRoot.value font.weight: 600 horizontalAlignment: Text.AlignLeft } } } } component WeatherStat: Row { id: weatherStat property string icon property string label property string value property color colour spacing: Appearance.spacing.small MaterialIcon { text: weatherStat.icon font.pointSize: Appearance.font.size.extraLarge color: weatherStat.colour } Column { StyledText { text: weatherStat.label font.pointSize: Appearance.font.size.smaller color: Colours.palette.m3onSurfaceVariant } StyledText { text: weatherStat.value font.pointSize: Appearance.font.size.small font.weight: 600 color: Colours.palette.m3onSurface } } } } ================================================ FILE: modules/dashboard/Wrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.filedialog import qs.config import qs.utils import Caelestia import Quickshell import QtQuick Item { id: root required property DrawerVisibilities visibilities readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false readonly property DashboardState dashState: DashboardState { reloadableId: "dashboardState" } readonly property FileDialog facePicker: FileDialog { title: qsTr("Select a profile picture") filterLabel: qsTr("Image files") filters: Images.validImageExtensions onAccepted: path => { if (CUtils.copyFile(Qt.resolvedUrl(path), Qt.resolvedUrl(`${Paths.home}/.face`))) Quickshell.execDetached(["notify-send", "-a", "caelestia-shell", "-u", "low", "-h", `STRING:image-path:${path}`, "Profile picture changed", `Profile picture changed to ${Paths.shortenHome(path)}`]); else Quickshell.execDetached(["notify-send", "-a", "caelestia-shell", "-u", "critical", "Unable to change profile picture", `Failed to change profile picture to ${Paths.shortenHome(path)}`]); } } readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 visible: height > 0 implicitHeight: 0 implicitWidth: content.implicitWidth onStateChanged: { if (state === "visible" && timer.running) { timer.triggered(); timer.stop(); } } states: State { name: "visible" when: root.visibilities.dashboard && Config.dashboard.enabled PropertyChanges { root.implicitHeight: content.implicitHeight } } transitions: [ Transition { from: "" to: "visible" Anim { target: root property: "implicitHeight" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } }, Transition { from: "visible" to: "" Anim { target: root property: "implicitHeight" easing.bezierCurve: Appearance.anim.curves.emphasized } } ] Timer { id: timer running: true interval: Appearance.anim.durations.extraLarge onTriggered: { content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); content.visible = true; } } Loader { id: content anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom visible: false active: true sourceComponent: Content { visibilities: root.visibilities state: root.dashState facePicker: root.facePicker } } } ================================================ FILE: modules/dashboard/dash/Calendar.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.effects import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Controls import QtQuick.Layouts CustomMouseArea { id: root required property var state readonly property int currMonth: state.currentDate.getMonth() readonly property int currYear: state.currentDate.getFullYear() function onWheel(event: WheelEvent): void { if (event.angleDelta.y > 0) root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); else if (event.angleDelta.y < 0) root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } anchors.left: parent.left anchors.right: parent.right implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 acceptedButtons: Qt.MiddleButton onClicked: root.state.currentDate = new Date() ColumnLayout { id: inner anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.small RowLayout { id: monthNavigationRow Layout.fillWidth: true spacing: Appearance.spacing.small Item { implicitWidth: implicitHeight implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 StateLayer { id: prevMonthStateLayer function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); } radius: Appearance.rounding.full } MaterialIcon { id: prevMonthText anchors.centerIn: parent text: "chevron_left" color: Colours.palette.m3tertiary font.pointSize: Appearance.font.size.normal font.weight: 700 } } Item { Layout.fillWidth: true implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 StateLayer { function onClicked(): void { root.state.currentDate = new Date(); } anchors.fill: monthYearDisplay anchors.margins: -Appearance.padding.small anchors.leftMargin: -Appearance.padding.normal anchors.rightMargin: -Appearance.padding.normal radius: Appearance.rounding.full disabled: { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); } } StyledText { id: monthYearDisplay anchors.centerIn: parent text: grid.title color: Colours.palette.m3primary font.pointSize: Appearance.font.size.normal font.weight: 500 font.capitalization: Font.Capitalize } } Item { implicitWidth: implicitHeight implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 StateLayer { id: nextMonthStateLayer function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } radius: Appearance.rounding.full } MaterialIcon { id: nextMonthText anchors.centerIn: parent text: "chevron_right" color: Colours.palette.m3tertiary font.pointSize: Appearance.font.size.normal font.weight: 700 } } } DayOfWeekRow { id: daysRow Layout.fillWidth: true locale: grid.locale delegate: StyledText { required property var model horizontalAlignment: Text.AlignHCenter text: model.shortName font.weight: 500 color: (model.day === 0 || model.day === 6) ? Colours.palette.m3secondary : Colours.palette.m3onSurfaceVariant } } Item { Layout.fillWidth: true implicitHeight: grid.implicitHeight MonthGrid { id: grid month: root.currMonth year: root.currYear anchors.fill: parent spacing: 3 locale: Qt.locale() delegate: Item { id: dayItem required property var model implicitWidth: implicitHeight implicitHeight: text.implicitHeight + Appearance.padding.small * 2 StyledText { id: text anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter text: grid.locale.toString(dayItem.model.day) color: { const dayOfWeek = dayItem.model.date.getUTCDay(); if (dayOfWeek === 0 || dayOfWeek === 6) return Colours.palette.m3secondary; return Colours.palette.m3onSurfaceVariant; } opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 font.pointSize: Appearance.font.size.normal font.weight: 500 } } } StyledRect { id: todayIndicator readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null property Item today onTodayItemChanged: { if (todayItem) today = todayItem; } x: today ? today.x + (today.width - implicitWidth) / 2 : 0 y: today?.y ?? 0 implicitWidth: today?.implicitWidth ?? 0 implicitHeight: today?.implicitHeight ?? 0 clip: true radius: Appearance.rounding.full color: Colours.palette.m3primary opacity: todayItem ? 1 : 0 scale: todayItem ? 1 : 0.7 Colouriser { x: -todayIndicator.x y: -todayIndicator.y implicitWidth: grid.width implicitHeight: grid.height source: grid sourceColor: Colours.palette.m3onSurface colorizationColor: Colours.palette.m3onPrimary } Behavior on opacity { Anim {} } Behavior on scale { Anim {} } Behavior on x { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Behavior on y { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } } } ================================================ FILE: modules/dashboard/dash/DateTime.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts Item { id: root anchors.top: parent.top anchors.bottom: parent.bottom implicitWidth: Config.dashboard.sizes.dateTimeWidth ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 0 StyledText { Layout.bottomMargin: -(font.pointSize * 0.4) Layout.alignment: Qt.AlignHCenter text: Time.hourStr color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.extraLarge font.family: Appearance.font.family.clock font.weight: 600 } StyledText { Layout.alignment: Qt.AlignHCenter text: "•••" color: Colours.palette.m3primary font.pointSize: Appearance.font.size.extraLarge * 0.9 font.family: Appearance.font.family.clock } StyledText { Layout.topMargin: -(font.pointSize * 0.4) Layout.alignment: Qt.AlignHCenter text: Time.minuteStr color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.extraLarge font.family: Appearance.font.family.clock font.weight: 600 } Loader { asynchronous: true Layout.alignment: Qt.AlignHCenter active: Config.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large font.family: Appearance.font.family.clock font.weight: 600 } } } } ================================================ FILE: modules/dashboard/dash/Media.qml ================================================ import qs.components import qs.services import qs.config import qs.utils import Caelestia.Services import QtQuick import QtQuick.Shapes Item { id: root property real playerProgress: { const active = Players.active; return active?.length ? active.position / active.length : 0; } anchors.top: parent.top anchors.bottom: parent.bottom implicitWidth: Config.dashboard.sizes.mediaWidth Behavior on playerProgress { Anim { duration: Appearance.anim.durations.large } } Timer { running: Players.active?.isPlaying ?? false interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() } ServiceRef { service: Audio.beatTracker } Shape { preferredRendererType: Shape.CurveRenderer ShapePath { fillColor: "transparent" strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) strokeWidth: Config.dashboard.sizes.mediaProgressThickness capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 sweepAngle: Config.dashboard.sizes.mediaProgressSweep } Behavior on strokeColor { CAnim {} } } ShapePath { fillColor: "transparent" strokeColor: Colours.palette.m3primary strokeWidth: Config.dashboard.sizes.mediaProgressThickness capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress } Behavior on strokeColor { CAnim {} } } } StyledClippingRect { id: cover anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small implicitHeight: width color: Colours.tPalette.m3surfaceContainerHigh radius: Infinity MaterialIcon { anchors.centerIn: parent grade: 200 text: "art_track" color: Colours.palette.m3onSurfaceVariant font.pointSize: (parent.width * 0.4) || 1 } Image { id: image anchors.fill: parent source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height } } StyledText { id: title anchors.top: cover.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Appearance.spacing.normal animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Colours.palette.m3primary font.pointSize: Appearance.font.size.normal width: parent.implicitWidth - Appearance.padding.large * 2 elide: Text.ElideRight } StyledText { id: album anchors.top: title.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Appearance.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small width: parent.implicitWidth - Appearance.padding.large * 2 elide: Text.ElideRight } StyledText { id: artist anchors.top: album.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Appearance.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") color: Colours.palette.m3secondary width: parent.implicitWidth - Appearance.padding.large * 2 elide: Text.ElideRight } Row { id: controls anchors.top: artist.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Appearance.spacing.smaller spacing: Appearance.spacing.small Control { function onClicked(): void { Players.active?.previous(); } icon: "skip_previous" canUse: Players.active?.canGoPrevious ?? false } Control { function onClicked(): void { Players.active?.togglePlaying(); } icon: Players.active?.isPlaying ? "pause" : "play_arrow" canUse: Players.active?.canTogglePlaying ?? false } Control { function onClicked(): void { Players.active?.next(); } icon: "skip_next" canUse: Players.active?.canGoNext ?? false } } AnimatedImage { id: bongocat anchors.top: controls.bottom anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right anchors.topMargin: Appearance.spacing.small anchors.bottomMargin: Appearance.padding.large anchors.margins: Appearance.padding.large * 2 playing: Players.active?.isPlaying ?? false speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit } component Control: StyledRect { id: control required property string icon required property bool canUse function onClicked(): void { } implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small implicitHeight: implicitWidth StateLayer { function onClicked(): void { control.onClicked(); } disabled: !control.canUse radius: Appearance.rounding.full } MaterialIcon { id: icon anchors.centerIn: parent anchors.verticalCenterOffset: font.pointSize * 0.05 animate: true text: control.icon color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline font.pointSize: Appearance.font.size.large } } } ================================================ FILE: modules/dashboard/dash/Resources.qml ================================================ import qs.components import qs.components.misc import qs.services import qs.config import QtQuick Row { id: root anchors.top: parent.top anchors.bottom: parent.bottom padding: Appearance.padding.large spacing: Appearance.spacing.normal Ref { service: SystemUsage } Resource { icon: "memory" value: SystemUsage.cpuPerc colour: Colours.palette.m3primary } Resource { icon: "memory_alt" value: SystemUsage.memPerc colour: Colours.palette.m3secondary } Resource { icon: "hard_disk" value: SystemUsage.storagePerc colour: Colours.palette.m3tertiary } component Resource: Item { id: res required property string icon required property real value required property color colour anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: Appearance.padding.large implicitWidth: icon.implicitWidth StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: icon.top anchors.bottomMargin: Appearance.spacing.small implicitWidth: Config.dashboard.sizes.resourceProgessThickness color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) radius: Appearance.rounding.full StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom implicitHeight: res.value * parent.height color: res.colour radius: Appearance.rounding.full } } MaterialIcon { id: icon anchors.bottom: parent.bottom text: res.icon color: res.colour } Behavior on value { Anim { duration: Appearance.anim.durations.large } } } } ================================================ FILE: modules/dashboard/dash/User.qml ================================================ import qs.components import qs.components.effects import qs.components.images import qs.components.filedialog import qs.services import qs.config import qs.utils import QtQuick Row { id: root required property DrawerVisibilities visibilities required property DashboardState state required property FileDialog facePicker padding: Appearance.padding.large spacing: Appearance.spacing.normal StyledClippingRect { implicitWidth: info.implicitHeight implicitHeight: info.implicitHeight radius: Appearance.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) MaterialIcon { anchors.centerIn: parent text: "person" fill: 1 grade: 200 font.pointSize: Math.floor(info.implicitHeight / 2) || 1 visible: pfp.status !== Image.Ready } CachingImage { id: pfp anchors.fill: parent path: `${Paths.home}/.face` } MouseArea { anchors.fill: parent hoverEnabled: true StyledRect { anchors.fill: parent color: Qt.alpha(Colours.palette.m3scrim, 0.5) opacity: parent.containsMouse ? 1 : 0 Behavior on opacity { Anim { duration: Appearance.anim.durations.expressiveFastSpatial } } } StyledRect { anchors.centerIn: parent implicitWidth: selectIcon.implicitHeight + Appearance.padding.small * 2 implicitHeight: selectIcon.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.normal color: Colours.palette.m3primary scale: parent.containsMouse ? 1 : 0.5 opacity: parent.containsMouse ? 1 : 0 StateLayer { function onClicked(): void { root.visibilities.launcher = false; root.facePicker.open(); } color: Colours.palette.m3onPrimary } MaterialIcon { id: selectIcon anchors.centerIn: parent anchors.horizontalCenterOffset: -font.pointSize * 0.02 text: "frame_person" color: Colours.palette.m3onPrimary font.pointSize: Appearance.font.size.extraLarge } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Behavior on opacity { Anim { duration: Appearance.anim.durations.expressiveFastSpatial } } } } } Column { id: info anchors.verticalCenter: parent.verticalCenter spacing: Appearance.spacing.normal Item { id: line implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) ColouredIcon { id: icon anchors.left: parent.left anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 source: SysInfo.osLogo implicitSize: Math.floor(Appearance.font.size.normal * 1.34) colour: Colours.palette.m3primary } StyledText { id: text anchors.verticalCenter: icon.verticalCenter anchors.left: icon.right anchors.leftMargin: icon.anchors.leftMargin text: `: ${SysInfo.osPrettyName || SysInfo.osName}` font.pointSize: Appearance.font.size.normal width: Config.dashboard.sizes.infoWidth elide: Text.ElideRight } } InfoLine { icon: "select_window_2" text: SysInfo.wm colour: Colours.palette.m3secondary } InfoLine { id: uptime icon: "timer" text: qsTr("up %1").arg(SysInfo.uptime) colour: Colours.palette.m3tertiary } } component InfoLine: Item { id: line required property string icon required property string text required property color colour implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight) MaterialIcon { id: icon anchors.left: parent.left anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 fill: 1 text: line.icon color: line.colour font.pointSize: Appearance.font.size.normal } StyledText { id: text anchors.verticalCenter: icon.verticalCenter anchors.left: icon.right anchors.leftMargin: icon.anchors.leftMargin text: `: ${line.text}` font.pointSize: Appearance.font.size.normal width: Config.dashboard.sizes.infoWidth elide: Text.ElideRight } } } ================================================ FILE: modules/dashboard/dash/Weather.qml ================================================ import qs.components import qs.services import qs.config import QtQuick Item { id: root anchors.centerIn: parent implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin Component.onCompleted: Weather.reload() MaterialIcon { id: icon anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left animate: true text: Weather.icon color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.extraLarge * 2 } Column { id: info anchors.verticalCenter: parent.verticalCenter anchors.left: icon.right anchors.leftMargin: Appearance.spacing.large spacing: Appearance.spacing.small StyledText { anchors.horizontalCenter: parent.horizontalCenter animate: true text: Weather.temp color: Colours.palette.m3primary font.pointSize: Appearance.font.size.extraLarge font.weight: 500 } StyledText { anchors.horizontalCenter: parent.horizontalCenter animate: true text: Weather.description elide: Text.ElideRight width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2) } } } ================================================ FILE: modules/drawers/Backgrounds.qml ================================================ import qs.config import qs.modules.osd as Osd import qs.modules.notifications as Notifications import qs.modules.session as Session import qs.modules.launcher as Launcher import qs.modules.dashboard as Dashboard import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities import qs.modules.sidebar as Sidebar import QtQuick import QtQuick.Shapes Shape { id: root required property Panels panels required property Item bar anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth preferredRendererType: Shape.CurveRenderer Osd.Background { wrapper: root.panels.osd startX: root.width - root.panels.session.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Notifications.Background { wrapper: root.panels.notifications sidebar: sidebar startX: root.width startY: 0 } Session.Background { wrapper: root.panels.session startX: root.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Launcher.Background { wrapper: root.panels.launcher startX: (root.width - wrapper.width) / 2 - rounding startY: root.height } Dashboard.Background { wrapper: root.panels.dashboard startX: (root.width - wrapper.width) / 2 - rounding startY: 0 } BarPopouts.Background { wrapper: root.panels.popouts invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height startX: wrapper.x startY: wrapper.y - rounding * sideRounding } Utilities.Background { wrapper: root.panels.utilities sidebar: sidebar startX: root.width startY: root.height } Sidebar.Background { id: sidebar wrapper: root.panels.sidebar panels: root.panels startX: root.width startY: root.panels.notifications.height } } ================================================ FILE: modules/drawers/Border.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import QtQuick import QtQuick.Effects Item { id: root required property Item bar anchors.fill: parent StyledRect { anchors.fill: parent color: Colours.palette.m3surface layer.enabled: true layer.effect: MultiEffect { maskSource: mask maskEnabled: true maskInverted: true maskThresholdMin: 0.5 maskSpreadAtMin: 1 } } Item { id: mask anchors.fill: parent layer.enabled: true visible: false Rectangle { anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: root.bar.implicitWidth radius: Config.border.rounding } } } ================================================ FILE: modules/drawers/Drawers.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.containers import qs.services import qs.config import qs.utils import qs.modules.bar import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick import QtQuick.Controls import QtQuick.Effects Variants { model: Screens.screens Scope { id: scope required property ShellScreen modelData readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name) Exclusions { screen: scope.modelData bar: bar } StyledWindow { id: win readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false readonly property int dragMaskPadding: { if (focusGrab.active || panels.popouts.isDetached) return 0; const mon = Hypr.monitorFor(screen); if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) return 0; const thresholds = []; for (const panel of ["dashboard", "launcher", "session", "sidebar"]) if (Config[panel].enabled) thresholds.push(Config[panel].dragThreshold); return Math.max(...thresholds); } onHasFullscreenChanged: { visibilities.launcher = false; visibilities.session = false; visibilities.dashboard = false; } screen: scope.modelData name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: Region { x: bar.clampedWidth + win.dragMaskPadding y: Config.border.clampedThickness + win.dragMaskPadding width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 intersection: Intersection.Xor regions: regions.instances } anchors.top: true anchors.bottom: true anchors.left: true anchors.right: true Variants { id: regions model: panels.children Region { required property Item modelData x: modelData.x + bar.implicitWidth y: modelData.y + Config.border.thickness width: modelData.width height: modelData.height intersection: Intersection.Subtract } } HyprlandFocusGrab { id: focusGrab 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) windows: [win] onCleared: { visibilities.launcher = false; visibilities.session = false; visibilities.sidebar = false; visibilities.dashboard = false; panels.popouts.hasCurrent = false; bar.closeTray(); } } StyledRect { anchors.fill: parent opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 color: Colours.palette.m3scrim Behavior on opacity { Anim {} } } Item { anchors.fill: parent opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 layer.enabled: true layer.effect: MultiEffect { shadowEnabled: true blurMax: 15 shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) } Border { bar: bar } Backgrounds { panels: panels bar: bar } } DrawerVisibilities { id: visibilities Component.onCompleted: Visibilities.load(scope.modelData, this) } Interactions { screen: scope.modelData popouts: panels.popouts visibilities: visibilities panels: panels bar: bar Panels { id: panels screen: scope.modelData visibilities: visibilities bar: bar } BarWrapper { id: bar anchors.top: parent.top anchors.bottom: parent.bottom screen: scope.modelData visibilities: visibilities popouts: panels.popouts disabled: scope.barDisabled Component.onCompleted: Visibilities.bars.set(scope.modelData, this) } } } } } ================================================ FILE: modules/drawers/Exclusions.qml ================================================ pragma ComponentBehavior: Bound import qs.components.containers import qs.config import qs.modules.bar as Bar import Quickshell import QtQuick Scope { id: root required property ShellScreen screen required property Bar.BarWrapper bar ExclusionZone { anchors.left: true exclusiveZone: root.bar.exclusiveZone } ExclusionZone { anchors.top: true } ExclusionZone { anchors.right: true } ExclusionZone { anchors.bottom: true } component ExclusionZone: StyledWindow { screen: root.screen name: "border-exclusion" exclusiveZone: Config.border.thickness mask: Region {} implicitWidth: 1 implicitHeight: 1 } } ================================================ FILE: modules/drawers/Interactions.qml ================================================ import qs.components import qs.components.controls import qs.config import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts import Quickshell import QtQuick import QtQuick.Controls CustomMouseArea { id: root required property ShellScreen screen required property BarPopouts.Wrapper popouts required property DrawerVisibilities visibilities required property Panels panels required property Bar.BarWrapper bar property point dragStart property bool dashboardShortcutActive property bool osdShortcutActive property bool utilitiesShortcutActive function withinPanelHeight(panel: Item, x: real, y: real): bool { const panelY = Config.border.thickness + panel.y; return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; } function withinPanelWidth(panel: Item, x: real, y: real): bool { const panelX = bar.implicitWidth + panel.x; return x >= panelX - Config.border.rounding && x <= panelX + panel.width + Config.border.rounding; } function inLeftPanel(panel: Item, x: real, y: real): bool { return x < bar.implicitWidth + panel.x + panel.width && withinPanelHeight(panel, x, y); } function inRightPanel(panel: Item, x: real, y: real): bool { return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); } function inTopPanel(panel: Item, x: real, y: real): bool { return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y); } function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); } function onWheel(event: WheelEvent): void { if (event.x < bar.implicitWidth) { bar.handleWheel(event.y, event.angleDelta); } } anchors.fill: parent hoverEnabled: true onPressed: event => dragStart = Qt.point(event.x, event.y) onContainsMouseChanged: { if (!containsMouse) { // Only hide if not activated by shortcut if (!osdShortcutActive) { visibilities.osd = false; root.panels.osd.hovered = false; } if (!dashboardShortcutActive) visibilities.dashboard = false; if (!utilitiesShortcutActive) visibilities.utilities = false; if (!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) { popouts.hasCurrent = false; bar.closeTray(); } if (Config.bar.showOnHover) bar.isHovered = false; } } onPositionChanged: event => { if (popouts.isDetached) return; const x = event.x; const y = event.y; const dragX = x - dragStart.x; const dragY = y - dragStart.y; // Show bar in non-exclusive mode on hover if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) bar.isHovered = true; // Show/hide bar on drag if (pressed && dragStart.x < bar.clampedWidth) { if (dragX > Config.bar.dragThreshold) visibilities.bar = true; else if (dragX < -Config.bar.dragThreshold) visibilities.bar = false; } if (panels.sidebar.width === 0) { // Show osd on hover const showOsd = inRightPanel(panels.osd, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { visibilities.osd = showOsd; root.panels.osd.hovered = showOsd; } else if (showOsd) { // If hovering over OSD area while in shortcut mode, transition to hover control osdShortcutActive = false; root.panels.osd.hovered = true; } const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); // Show/hide session on drag if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) visibilities.session = false; // Show sidebar on drag if in session area and session is nearly fully visible if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) visibilities.sidebar = true; } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { // Show sidebar on drag if not in session area visibilities.sidebar = true; } } else { const outOfSidebar = x < width - panels.sidebar.width; // Show osd on hover const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { visibilities.osd = showOsd; root.panels.osd.hovered = showOsd; } else if (showOsd) { // If hovering over OSD area while in shortcut mode, transition to hover control osdShortcutActive = false; root.panels.osd.hovered = true; } // Show/hide session on drag if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) visibilities.session = false; } // Hide sidebar on drag if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold) visibilities.sidebar = false; } // Show launcher on hover, or show/hide on drag if hover is disabled if (Config.launcher.showOnHover) { if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y)) visibilities.launcher = true; } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) { if (dragY < -Config.launcher.dragThreshold) visibilities.launcher = true; else if (dragY > Config.launcher.dragThreshold) visibilities.launcher = false; } // Show dashboard on hover const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y); // Always update visibility based on hover if not in shortcut mode if (!dashboardShortcutActive) { visibilities.dashboard = showDashboard; } else if (showDashboard) { // If hovering over dashboard area while in shortcut mode, transition to hover control dashboardShortcutActive = false; } // Show/hide dashboard on drag (for touchscreen devices) if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) { if (dragY > Config.dashboard.dragThreshold) visibilities.dashboard = true; else if (dragY < -Config.dashboard.dragThreshold) visibilities.dashboard = false; } // Show utilities on hover const showUtilities = inBottomPanel(panels.utilities, x, y, true); // Always update visibility based on hover if not in shortcut mode if (!utilitiesShortcutActive) { visibilities.utilities = showUtilities; } else if (showUtilities) { // If hovering over utilities area while in shortcut mode, transition to hover control utilitiesShortcutActive = false; } // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { popouts.hasCurrent = false; bar.closeTray(); } } // Monitor individual visibility changes Connections { function onLauncherChanged() { // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { root.dashboardShortcutActive = false; root.osdShortcutActive = false; root.utilitiesShortcutActive = false; // Also hide dashboard and OSD if they're not being hovered const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inDashboardArea) { root.visibilities.dashboard = false; } if (!inOsdArea) { root.visibilities.osd = false; root.panels.osd.hovered = false; } } } function onDashboardChanged() { if (root.visibilities.dashboard) { // Dashboard became visible, immediately check if this should be shortcut mode const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); if (!inDashboardArea) { root.dashboardShortcutActive = true; } } else { // Dashboard hidden, clear shortcut flag root.dashboardShortcutActive = false; } } function onOsdChanged() { if (root.visibilities.osd) { // OSD became visible, immediately check if this should be shortcut mode const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inOsdArea) { root.osdShortcutActive = true; } } else { // OSD hidden, clear shortcut flag root.osdShortcutActive = false; } } function onUtilitiesChanged() { if (root.visibilities.utilities) { // Utilities became visible, immediately check if this should be shortcut mode const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY); if (!inUtilitiesArea) { root.utilitiesShortcutActive = true; } } else { // Utilities hidden, clear shortcut flag root.utilitiesShortcutActive = false; } } target: root.visibilities } } ================================================ FILE: modules/drawers/Panels.qml ================================================ import qs.components import qs.config import qs.modules.osd as Osd import qs.modules.notifications as Notifications import qs.modules.session as Session import qs.modules.launcher as Launcher import qs.modules.dashboard as Dashboard import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts import qs.modules.sidebar as Sidebar import Quickshell import QtQuick Item { id: root required property ShellScreen screen required property DrawerVisibilities visibilities required property Bar.BarWrapper bar readonly property alias osd: osd readonly property alias notifications: notifications readonly property alias session: session readonly property alias launcher: launcher readonly property alias dashboard: dashboard readonly property alias popouts: popouts readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth Osd.Wrapper { id: osd clip: session.width > 0 || sidebar.width > 0 screen: root.screen visibilities: root.visibilities anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: session.width + sidebar.width } Notifications.Wrapper { id: notifications visibilities: root.visibilities sidebarPanel: sidebar osdPanel: osd sessionPanel: session anchors.top: parent.top anchors.right: parent.right } Session.Wrapper { id: session clip: sidebar.width > 0 visibilities: root.visibilities panels: root anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: sidebar.width } Launcher.Wrapper { id: launcher screen: root.screen visibilities: root.visibilities panels: root anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom } Dashboard.Wrapper { id: dashboard visibilities: root.visibilities anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top } BarPopouts.Wrapper { id: popouts screen: root.screen x: isDetached ? (root.width - nonAnimWidth) / 2 : 0 y: { if (isDetached) return (root.height - nonAnimHeight) / 2; const off = currentCenter - Config.border.thickness - nonAnimHeight / 2; const diff = root.height - Math.floor(off + nonAnimHeight); if (diff < 0) return off + diff; return Math.max(off, 0); } } Utilities.Wrapper { id: utilities visibilities: root.visibilities sidebar: sidebar popouts: popouts anchors.bottom: parent.bottom anchors.right: parent.right } Toasts.Toasts { id: toasts anchors.bottom: sidebar.visible ? parent.bottom : utilities.top anchors.right: sidebar.left anchors.margins: Appearance.padding.normal } Sidebar.Wrapper { id: sidebar visibilities: root.visibilities panels: root anchors.top: notifications.bottom anchors.bottom: utilities.top anchors.right: parent.right } } ================================================ FILE: modules/launcher/AppList.qml ================================================ pragma ComponentBehavior: Bound import qs.modules.launcher.items import qs.modules.launcher.services import qs.components import qs.components.controls import qs.components.containers import qs.services import qs.config import Quickshell import QtQuick StyledListView { id: root required property StyledTextField search required property DrawerVisibilities visibilities model: ScriptModel { id: model onValuesChanged: root.currentIndex = 0 } spacing: Appearance.spacing.small orientation: Qt.Vertical implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing preferredHighlightBegin: 0 preferredHighlightEnd: height highlightRangeMode: ListView.ApplyRange highlightFollowsCurrentItem: false highlight: StyledRect { radius: Appearance.rounding.normal color: Colours.palette.m3onSurface opacity: 0.08 y: root.currentItem?.y ?? 0 implicitWidth: root.width implicitHeight: root.currentItem?.implicitHeight ?? 0 Behavior on y { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } state: { const text = search.text; const prefix = Config.launcher.actionPrefix; if (text.startsWith(prefix)) { for (const action of ["calc", "scheme", "variant"]) if (text.startsWith(`${prefix}${action} `)) return action; return "actions"; } return "apps"; } onStateChanged: { if (state === "scheme" || state === "variant") Schemes.reload(); } states: [ State { name: "apps" PropertyChanges { model.values: Apps.search(search.text) root.delegate: appItem } }, State { name: "actions" PropertyChanges { model.values: Actions.query(search.text) root.delegate: actionItem } }, State { name: "calc" PropertyChanges { model.values: [0] root.delegate: calcItem } }, State { name: "scheme" PropertyChanges { model.values: Schemes.query(search.text) root.delegate: schemeItem } }, State { name: "variant" PropertyChanges { model.values: M3Variants.query(search.text) root.delegate: variantItem } } ] transitions: Transition { SequentialAnimation { ParallelAnimation { Anim { target: root property: "opacity" from: 1 to: 0 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardAccel } Anim { target: root property: "scale" from: 1 to: 0.9 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardAccel } } PropertyAction { targets: [model, root] properties: "values,delegate" } ParallelAnimation { Anim { target: root property: "opacity" from: 0 to: 1 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { target: root property: "scale" from: 0.9 to: 1 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardDecel } } PropertyAction { targets: [root.add, root.remove] property: "enabled" value: true } } } StyledScrollBar.vertical: StyledScrollBar { flickable: root } add: Transition { enabled: !root.state Anim { properties: "opacity,scale" from: 0 to: 1 } } remove: Transition { enabled: !root.state Anim { properties: "opacity,scale" from: 1 to: 0 } } move: Transition { Anim { property: "y" } Anim { properties: "opacity,scale" to: 1 } } addDisplaced: Transition { Anim { property: "y" duration: Appearance.anim.durations.small } Anim { properties: "opacity,scale" to: 1 } } displaced: Transition { Anim { property: "y" } Anim { properties: "opacity,scale" to: 1 } } Component { id: appItem AppItem { visibilities: root.visibilities } } Component { id: actionItem ActionItem { list: root } } Component { id: calcItem CalcItem { list: root } } Component { id: schemeItem SchemeItem { list: root } } Component { id: variantItem VariantItem { list: root } } } ================================================ FILE: modules/launcher/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface PathArc { relativeX: root.rounding relativeY: -root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: -(root.wrapper.height - root.roundingY * 2) } PathArc { relativeX: root.rounding relativeY: -root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) } PathLine { relativeX: root.wrapper.width - root.rounding * 2 relativeY: 0 } PathArc { relativeX: root.rounding relativeY: root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) } PathLine { relativeX: 0 relativeY: root.wrapper.height - root.roundingY * 2 } PathArc { relativeX: root.rounding relativeY: root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } Behavior on fillColor { CAnim {} } } ================================================ FILE: modules/launcher/Content.qml ================================================ pragma ComponentBehavior: Bound import qs.modules.launcher.services import qs.components import qs.components.controls import qs.services import qs.config import QtQuick Item { id: root required property DrawerVisibilities visibilities required property var panels required property real maxHeight readonly property int padding: Appearance.padding.large readonly property int rounding: Appearance.rounding.large implicitWidth: listWrapper.width + padding * 2 implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 Item { id: listWrapper implicitWidth: list.width implicitHeight: list.height + root.padding anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: searchWrapper.top anchors.bottomMargin: root.padding ContentList { id: list content: root visibilities: root.visibilities panels: root.panels maxHeight: root.maxHeight - searchWrapper.implicitHeight - root.padding * 3 search: search padding: root.padding rounding: root.rounding } } StyledRect { id: searchWrapper color: Colours.layer(Colours.palette.m3surfaceContainer, 2) radius: Appearance.rounding.full anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: root.padding implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight) MaterialIcon { id: searchIcon anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: root.padding text: "search" color: Colours.palette.m3onSurfaceVariant } StyledTextField { id: search anchors.left: searchIcon.right anchors.right: clearIcon.left anchors.leftMargin: Appearance.spacing.small anchors.rightMargin: Appearance.spacing.small topPadding: Appearance.padding.larger bottomPadding: Appearance.padding.larger placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) onAccepted: { const currentItem = list.currentList?.currentItem; if (currentItem) { if (list.showWallpapers) { if (Colours.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent) Wallpapers.previewColourLock = true; Wallpapers.setWallpaper(currentItem.modelData.path); root.visibilities.launcher = false; } else if (text.startsWith(Config.launcher.actionPrefix)) { if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) currentItem.onClicked(); else currentItem.modelData.onClicked(list.currentList); } else { Apps.launch(currentItem.modelData); root.visibilities.launcher = false; } } } Keys.onUpPressed: list.currentList?.decrementCurrentIndex() Keys.onDownPressed: list.currentList?.incrementCurrentIndex() Keys.onEscapePressed: root.visibilities.launcher = false Keys.onPressed: event => { if (!Config.launcher.vimKeybinds) return; if (event.modifiers & Qt.ControlModifier) { if (event.key === Qt.Key_J) { list.currentList?.incrementCurrentIndex(); event.accepted = true; } else if (event.key === Qt.Key_K) { list.currentList?.decrementCurrentIndex(); event.accepted = true; } } else if (event.key === Qt.Key_Tab) { list.currentList?.incrementCurrentIndex(); event.accepted = true; } else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { list.currentList?.decrementCurrentIndex(); event.accepted = true; } } Component.onCompleted: forceActiveFocus() Connections { function onLauncherChanged(): void { if (!root.visibilities.launcher) search.text = ""; } function onSessionChanged(): void { if (!root.visibilities.session) search.forceActiveFocus(); } target: root.visibilities } } MaterialIcon { id: clearIcon anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: root.padding width: search.text ? implicitWidth : implicitWidth / 2 opacity: { if (!search.text) return 0; if (mouse.pressed) return 0.7; if (mouse.containsMouse) return 0.8; return 1; } text: "close" color: Colours.palette.m3onSurfaceVariant MouseArea { id: mouse anchors.fill: parent hoverEnabled: true cursorShape: search.text ? Qt.PointingHandCursor : undefined onClicked: search.text = "" } Behavior on width { Anim { duration: Appearance.anim.durations.small } } Behavior on opacity { Anim { duration: Appearance.anim.durations.small } } } } } ================================================ FILE: modules/launcher/ContentList.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import QtQuick Item { id: root required property var content required property DrawerVisibilities visibilities required property var panels required property real maxHeight required property StyledTextField search required property int padding required property int rounding readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom clip: true state: showWallpapers ? "wallpapers" : "apps" states: [ State { name: "apps" PropertyChanges { root.implicitWidth: Config.launcher.sizes.itemWidth root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) appList.active: true } AnchorChanges { anchors.left: root.parent.left anchors.right: root.parent.right } }, State { name: "wallpapers" PropertyChanges { root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) root.implicitHeight: Config.launcher.sizes.wallpaperHeight wallpaperList.active: true } } ] Behavior on state { SequentialAnimation { Anim { target: root property: "opacity" from: 1 to: 0 duration: Appearance.anim.durations.small } PropertyAction {} Anim { target: root property: "opacity" from: 0 to: 1 duration: Appearance.anim.durations.small } } } Loader { id: appList active: false anchors.fill: parent sourceComponent: AppList { search: root.search visibilities: root.visibilities } } Loader { id: wallpaperList asynchronous: true active: false anchors.top: parent.top anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter sourceComponent: WallpaperList { search: root.search visibilities: root.visibilities panels: root.panels content: root.content } } Row { id: empty opacity: root.currentList?.count === 0 ? 1 : 0 scale: root.currentList?.count === 0 ? 1 : 0.5 spacing: Appearance.spacing.normal padding: Appearance.padding.large anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter MaterialIcon { text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } Column { anchors.verticalCenter: parent.verticalCenter StyledText { text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.larger font.weight: 500 } StyledText { 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") color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.normal } } Behavior on opacity { Anim {} } Behavior on scale { Anim {} } } Behavior on implicitWidth { enabled: root.visibilities.launcher Anim { duration: Appearance.anim.durations.large easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } } Behavior on implicitHeight { enabled: root.visibilities.launcher Anim { duration: Appearance.anim.durations.large easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } } } ================================================ FILE: modules/launcher/WallpaperList.qml ================================================ pragma ComponentBehavior: Bound import "items" import qs.components.controls import qs.services import qs.config import Quickshell import QtQuick PathView { id: root required property StyledTextField search required property var visibilities required property var panels required property var content readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 readonly property int numItems: { const screen = (QsWindow.window as QsWindow)?.screen; if (!screen) return 0; // Screen width - 4x outer rounding - 2x max side thickness (cause centered) const barMargins = Math.max(Config.border.thickness, panels.bar.implicitWidth); let outerMargins = 0; if (panels.popouts.hasCurrent && panels.popouts.currentCenter + panels.popouts.nonAnimHeight / 2 > screen.height - content.implicitHeight - Config.border.thickness * 2) outerMargins = panels.popouts.nonAnimWidth; if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins) outerMargins = panels.utilities.implicitWidth; const maxWidth = screen.width - Config.border.rounding * 4 - (barMargins + outerMargins) * 2; if (maxWidth <= 0) return 0; const maxItemsOnScreen = Math.floor(maxWidth / itemWidth); const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length); if (visible === 2) return 1; if (visible > 1 && visible % 2 === 0) return visible - 1; return visible; } model: ScriptModel { id: scriptModel readonly property string search: root.search.text.split(" ").slice(1).join(" ") values: Wallpapers.query(search) onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent) } Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent) Component.onDestruction: Wallpapers.stopPreview() onCurrentItemChanged: { if (currentItem) Wallpapers.preview((currentItem as WallpaperItem).modelData.path); } implicitWidth: Math.min(numItems, count) * itemWidth pathItemCount: numItems cacheItemCount: 4 snapMode: PathView.SnapToItem preferredHighlightBegin: 0.5 preferredHighlightEnd: 0.5 highlightRangeMode: PathView.StrictlyEnforceRange delegate: WallpaperItem { visibilities: root.visibilities } path: Path { startY: root.height / 2 PathAttribute { name: "z" value: 0 } PathLine { x: root.width / 2 relativeY: 0 } PathAttribute { name: "z" value: 1 } PathLine { x: root.width relativeY: 0 } } } ================================================ FILE: modules/launcher/Wrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import Quickshell import QtQuick Item { id: root required property ShellScreen screen required property DrawerVisibilities visibilities required property var panels readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled property int contentHeight readonly property real maxHeight: { let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; if (visibilities.dashboard) max -= panels.dashboard.nonAnimHeight; return max; } onMaxHeightChanged: timer.start() visible: height > 0 implicitHeight: 0 implicitWidth: content.implicitWidth onShouldBeActiveChanged: { if (shouldBeActive) { timer.stop(); hideAnim.stop(); showAnim.start(); } else { showAnim.stop(); hideAnim.start(); } } SequentialAnimation { id: showAnim Anim { target: root property: "implicitHeight" to: root.contentHeight duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } ScriptAction { script: root.implicitHeight = Qt.binding(() => content.implicitHeight) } } SequentialAnimation { id: hideAnim ScriptAction { script: root.implicitHeight = root.implicitHeight } Anim { target: root property: "implicitHeight" to: 0 easing.bezierCurve: Appearance.anim.curves.emphasized } } Connections { function onEnabledChanged(): void { timer.start(); } function onMaxShownChanged(): void { timer.start(); } target: Config.launcher } Connections { function onValuesChanged(): void { if (DesktopEntries.applications.values.length < Config.launcher.maxShown) timer.start(); } target: DesktopEntries.applications } Timer { id: timer interval: Appearance.anim.durations.extraLarge onRunningChanged: { if (running && !root.shouldBeActive) { content.visible = false; content.active = true; } else { root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); content.active = Qt.binding(() => root.shouldBeActive || root.visible); content.visible = true; if (showAnim.running) { showAnim.stop(); showAnim.start(); } } } } Loader { id: content anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter visible: false active: false Component.onCompleted: timer.start() sourceComponent: Content { visibilities: root.visibilities panels: root.panels maxHeight: root.maxHeight Component.onCompleted: root.contentHeight = implicitHeight } } } ================================================ FILE: modules/launcher/items/ActionItem.qml ================================================ import qs.components import qs.services import qs.config import QtQuick Item { id: root required property var modelData required property var list implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { function onClicked(): void { root.modelData?.onClicked(root.list); } radius: Appearance.rounding.normal } Item { anchors.fill: parent anchors.leftMargin: Appearance.padding.larger anchors.rightMargin: Appearance.padding.larger anchors.margins: Appearance.padding.smaller MaterialIcon { id: icon text: root.modelData?.icon ?? "" font.pointSize: Appearance.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } Item { anchors.left: icon.right anchors.leftMargin: Appearance.spacing.normal anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width implicitHeight: name.implicitHeight + desc.implicitHeight StyledText { id: name text: root.modelData?.name ?? "" font.pointSize: Appearance.font.size.normal } StyledText { id: desc text: root.modelData?.desc ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight width: root.width - icon.width - Appearance.rounding.normal * 2 anchors.top: name.bottom } } } } ================================================ FILE: modules/launcher/items/AppItem.qml ================================================ import qs.modules.launcher.services import qs.components import qs.services import qs.config import qs.utils import Quickshell import Quickshell.Widgets import QtQuick Item { id: root required property DesktopEntry modelData required property DrawerVisibilities visibilities implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { function onClicked(): void { Apps.launch(root.modelData); root.visibilities.launcher = false; } radius: Appearance.rounding.normal } Item { anchors.fill: parent anchors.leftMargin: Appearance.padding.larger anchors.rightMargin: Appearance.padding.larger anchors.margins: Appearance.padding.smaller IconImage { id: icon asynchronous: true source: Quickshell.iconPath(root.modelData?.icon, "image-missing") implicitSize: parent.height * 0.8 anchors.verticalCenter: parent.verticalCenter } Item { anchors.left: icon.right anchors.leftMargin: Appearance.spacing.normal anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width - favouriteIcon.width implicitHeight: name.implicitHeight + comment.implicitHeight StyledText { id: name text: root.modelData?.name ?? "" font.pointSize: Appearance.font.size.normal } StyledText { id: comment text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight width: root.width - icon.width - favouriteIcon.width - Appearance.rounding.normal * 2 anchors.top: name.bottom } } Loader { id: favouriteIcon asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right active: root.modelData && Strings.testRegexList(Config.launcher.favouriteApps, root.modelData.id) sourceComponent: MaterialIcon { text: "favorite" fill: 1 color: Colours.palette.m3primary } } } } ================================================ FILE: modules/launcher/items/CalcItem.qml ================================================ import qs.components import qs.services import qs.config import Caelestia import Quickshell import QtQuick import QtQuick.Layouts Item { id: root required property var list readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) function onClicked(): void { Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); root.list.visibilities.launcher = false; } onMathChanged: { if (math.length > 0) Qalculator.evalAsync(math); } implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { function onClicked(): void { root.onClicked(); } radius: Appearance.rounding.normal } RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.larger spacing: Appearance.spacing.normal MaterialIcon { text: "function" font.pointSize: Appearance.font.size.extraLarge Layout.alignment: Qt.AlignVCenter } StyledText { id: result color: { if (text.includes("error: ") || text.includes("warning: ")) return Colours.palette.m3error; if (!root.math) return Colours.palette.m3onSurfaceVariant; return Colours.palette.m3onSurface; } text: root.math.length > 0 ? (Qalculator.result || qsTr("Calculating...")) : qsTr("Type an expression to calculate") elide: Text.ElideLeft Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter } StyledRect { color: Colours.palette.m3tertiary radius: Appearance.rounding.normal clip: true implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 Layout.alignment: Qt.AlignVCenter StateLayer { id: stateLayer function onClicked(): void { Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); root.list.visibilities.launcher = false; } color: Colours.palette.m3onTertiary } StyledText { id: label anchors.verticalCenter: parent.verticalCenter anchors.right: icon.left anchors.rightMargin: Appearance.spacing.small text: qsTr("Open in calculator") color: Colours.palette.m3onTertiary font.pointSize: Appearance.font.size.normal opacity: stateLayer.containsMouse ? 1 : 0 Behavior on opacity { Anim {} } } MaterialIcon { id: icon anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: Appearance.padding.normal text: "open_in_new" color: Colours.palette.m3onTertiary font.pointSize: Appearance.font.size.large } Behavior on implicitWidth { Anim { easing.bezierCurve: Appearance.anim.curves.emphasized } } } } } ================================================ FILE: modules/launcher/items/SchemeItem.qml ================================================ import qs.modules.launcher.services import qs.components import qs.services import qs.config import QtQuick Item { id: root required property Schemes.Scheme modelData required property var list implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { function onClicked(): void { root.modelData?.onClicked(root.list); } radius: Appearance.rounding.normal } Item { anchors.fill: parent anchors.leftMargin: Appearance.padding.larger anchors.rightMargin: Appearance.padding.larger anchors.margins: Appearance.padding.smaller StyledRect { id: preview anchors.verticalCenter: parent.verticalCenter border.width: 1 border.color: Qt.alpha(`#${root.modelData?.colours?.outline}`, 0.5) color: `#${root.modelData?.colours?.surface}` radius: Appearance.rounding.full implicitWidth: parent.height * 0.8 implicitHeight: parent.height * 0.8 Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right implicitWidth: parent.implicitWidth / 2 clip: true StyledRect { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right implicitWidth: preview.implicitWidth color: `#${root.modelData?.colours?.primary}` radius: Appearance.rounding.full } } } Column { anchors.left: preview.right anchors.leftMargin: Appearance.spacing.normal anchors.verticalCenter: parent.verticalCenter width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) spacing: 0 StyledText { text: root.modelData?.flavour ?? "" font.pointSize: Appearance.font.size.normal } StyledText { text: root.modelData?.name ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight anchors.left: parent.left anchors.right: parent.right } } Loader { id: current asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter active: `${root.modelData?.name} ${root.modelData?.flavour}` === Schemes.currentScheme sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.large } } } } ================================================ FILE: modules/launcher/items/VariantItem.qml ================================================ import qs.modules.launcher.services import qs.components import qs.services import qs.config import QtQuick Item { id: root required property M3Variants.Variant modelData required property var list implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { function onClicked(): void { root.modelData?.onClicked(root.list); } radius: Appearance.rounding.normal } Item { anchors.fill: parent anchors.leftMargin: Appearance.padding.larger anchors.rightMargin: Appearance.padding.larger anchors.margins: Appearance.padding.smaller MaterialIcon { id: icon text: root.modelData?.icon ?? "" font.pointSize: Appearance.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } Column { anchors.left: icon.right anchors.leftMargin: Appearance.spacing.larger anchors.verticalCenter: icon.verticalCenter width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) spacing: 0 StyledText { text: root.modelData?.name ?? "" font.pointSize: Appearance.font.size.normal } StyledText { text: root.modelData?.description ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight anchors.left: parent.left anchors.right: parent.right } } Loader { id: current asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter active: root.modelData?.variant === Schemes.currentVariant sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.large } } } } ================================================ FILE: modules/launcher/items/WallpaperItem.qml ================================================ import qs.components import qs.components.effects import qs.components.images import qs.services import qs.config import Caelestia.Models import QtQuick Item { id: root required property FileSystemEntry modelData required property DrawerVisibilities visibilities scale: 0.5 opacity: 0 z: PathView.z ?? 0 // qmllint disable missing-property Component.onCompleted: { scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); opacity = Qt.binding(() => PathView.onPath ? 1 : 0); } implicitWidth: image.width + Appearance.padding.larger * 2 implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal StateLayer { function onClicked(): void { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; } radius: Appearance.rounding.normal } Elevation { anchors.fill: image radius: image.radius opacity: root.PathView.isCurrentItem ? 1 : 0 level: 4 Behavior on opacity { Anim {} } } StyledClippingRect { id: image anchors.horizontalCenter: parent.horizontalCenter y: Appearance.padding.large color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal implicitWidth: Config.launcher.sizes.wallpaperWidth implicitHeight: implicitWidth / 16 * 9 MaterialIcon { anchors.centerIn: parent text: "image" color: Colours.tPalette.m3outline font.pointSize: Appearance.font.size.extraLarge * 2 font.weight: 600 } CachingImage { path: root.modelData.path smooth: !root.PathView.view.moving cache: true anchors.fill: parent } } StyledText { id: label anchors.top: image.bottom anchors.topMargin: Appearance.spacing.small / 2 anchors.horizontalCenter: parent.horizontalCenter width: image.width - Appearance.padding.normal * 2 horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight renderType: Text.QtRendering text: root.modelData.relativePath font.pointSize: Appearance.font.size.normal } Behavior on scale { Anim {} } Behavior on opacity { Anim {} } } ================================================ FILE: modules/launcher/services/Actions.qml ================================================ pragma Singleton import ".." import qs.services import qs.config import qs.utils import Quickshell import QtQuick Searcher { id: root function transformSearch(search: string): string { return search.slice(Config.launcher.actionPrefix.length); } list: variants.instances useFuzzy: Config.launcher.useFuzzy.actions Variants { id: variants model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false))) Action {} } component Action: QtObject { required property var modelData readonly property string name: modelData.name ?? qsTr("Unnamed") readonly property string desc: modelData.description ?? qsTr("No description") readonly property string icon: modelData.icon ?? "help_outline" readonly property list command: modelData.command ?? [] readonly property bool enabled: modelData.enabled ?? true readonly property bool dangerous: modelData.dangerous ?? false function onClicked(list: AppList): void { if (command.length === 0) return; if (command[0] === "autocomplete" && command.length > 1) { list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; } else if (command[0] === "setMode" && command.length > 1) { list.visibilities.launcher = false; Colours.setMode(command[1]); } else { list.visibilities.launcher = false; Quickshell.execDetached(command); } } } } ================================================ FILE: modules/launcher/services/Apps.qml ================================================ pragma Singleton import qs.config import qs.utils import Caelestia import Quickshell Searcher { id: root function launch(entry: DesktopEntry): void { appDb.incrementFrequency(entry.id); if (entry.runInTerminal) Quickshell.execDetached({ command: ["app2unit", "--", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], workingDirectory: entry.workingDirectory }); else Quickshell.execDetached({ command: ["app2unit", "--", ...entry.command], workingDirectory: entry.workingDirectory }); } function search(search: string): list { const prefix = Config.launcher.specialPrefix; if (search.startsWith(`${prefix}i `)) { keys = ["id", "name"]; weights = [0.9, 0.1]; } else if (search.startsWith(`${prefix}c `)) { keys = ["categories", "name"]; weights = [0.9, 0.1]; } else if (search.startsWith(`${prefix}d `)) { keys = ["comment", "name"]; weights = [0.9, 0.1]; } else if (search.startsWith(`${prefix}e `)) { keys = ["execString", "name"]; weights = [0.9, 0.1]; } else if (search.startsWith(`${prefix}w `)) { keys = ["startupClass", "name"]; weights = [0.9, 0.1]; } else if (search.startsWith(`${prefix}g `)) { keys = ["genericName", "name"]; weights = [0.9, 0.1]; } else if (search.startsWith(`${prefix}k `)) { keys = ["keywords", "name"]; weights = [0.9, 0.1]; } else { keys = ["name"]; weights = [1]; if (!search.startsWith(`${prefix}t `)) return query(search).map(e => e.entry); } const results = query(search.slice(prefix.length + 2)).map(e => e.entry); if (search.startsWith(`${prefix}t `)) return results.filter(a => a.runInTerminal); return results; } function selector(item: var): string { return keys.map(k => item[k]).join(" "); } list: appDb.apps useFuzzy: Config.launcher.useFuzzy.apps AppDb { id: appDb path: `${Paths.state}/apps.sqlite` favouriteApps: Config.launcher.favouriteApps entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(Config.launcher.hiddenApps, a.id)) } } ================================================ FILE: modules/launcher/services/M3Variants.qml ================================================ pragma Singleton import ".." import qs.config import qs.utils import Quickshell import QtQuick Searcher { id: root function transformSearch(search: string): string { return search.slice(`${Config.launcher.actionPrefix}variant `.length); } list: [ Variant { variant: "vibrant" icon: "sentiment_very_dissatisfied" name: qsTr("Vibrant") description: qsTr("A high chroma palette. The primary palette's chroma is at maximum.") }, Variant { variant: "tonalspot" icon: "android" name: qsTr("Tonal Spot") description: qsTr("Default for Material theme colours. A pastel palette with a low chroma.") }, Variant { variant: "expressive" icon: "compare_arrows" name: qsTr("Expressive") description: qsTr("A medium chroma palette. The primary palette's hue is different from the seed colour, for variety.") }, Variant { variant: "fidelity" icon: "compare" name: qsTr("Fidelity") description: qsTr("Matches the seed colour, even if the seed colour is very bright (high chroma).") }, Variant { variant: "content" icon: "sentiment_calm" name: qsTr("Content") description: qsTr("Almost identical to fidelity.") }, Variant { variant: "fruitsalad" icon: "nutrition" name: qsTr("Fruit Salad") description: qsTr("A playful theme - the seed colour's hue does not appear in the theme.") }, Variant { variant: "rainbow" icon: "looks" name: qsTr("Rainbow") description: qsTr("A playful theme - the seed colour's hue does not appear in the theme.") }, Variant { variant: "neutral" icon: "contrast" name: qsTr("Neutral") description: qsTr("Close to grayscale, a hint of chroma.") }, Variant { variant: "monochrome" icon: "filter_b_and_w" name: qsTr("Monochrome") description: qsTr("All colours are grayscale, no chroma.") } ] useFuzzy: Config.launcher.useFuzzy.variants component Variant: QtObject { required property string variant required property string icon required property string name required property string description function onClicked(list: AppList): void { list.visibilities.launcher = false; Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); } } } ================================================ FILE: modules/launcher/services/Schemes.qml ================================================ pragma Singleton import ".." import qs.config import qs.utils import Quickshell import Quickshell.Io import QtQuick Searcher { id: root property string currentScheme property string currentVariant function transformSearch(search: string): string { return search.slice(`${Config.launcher.actionPrefix}scheme `.length); } function selector(item: var): string { return `${item.name} ${item.flavour}`; } function reload(): void { getCurrent.running = true; } list: schemes.instances useFuzzy: Config.launcher.useFuzzy.schemes keys: ["name", "flavour"] weights: [0.9, 0.1] Variants { id: schemes Scheme {} } Process { id: getSchemes running: true command: ["caelestia", "scheme", "list"] stdout: StdioCollector { onStreamFinished: { const schemeData = JSON.parse(text); const list = Object.entries(schemeData).map(([name, f]) => Object.entries(f).map(([flavour, colours]) => ({ name, flavour, colours }))); const flat = []; for (const s of list) for (const f of s) flat.push(f); schemes.model = flat.sort((a, b) => String(a.name + a.flavour).localeCompare((b.name + b.flavour))); } } } Process { id: getCurrent running: true command: ["caelestia", "scheme", "get", "-nfv"] stdout: StdioCollector { onStreamFinished: { const [name, flavour, variant] = text.trim().split("\n"); root.currentScheme = `${name} ${flavour}`; root.currentVariant = variant; } } } component Scheme: QtObject { required property var modelData readonly property string name: modelData.name readonly property string flavour: modelData.flavour readonly property var colours: modelData.colours function onClicked(list: AppList): void { list.visibilities.launcher = false; Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]); } } } ================================================ FILE: modules/lock/Center.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.components.images import qs.services import qs.config import qs.utils import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property var lock readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale Layout.preferredWidth: centerWidth Layout.fillWidth: false Layout.fillHeight: true spacing: Appearance.spacing.large * 2 RowLayout { Layout.alignment: Qt.AlignHCenter spacing: Appearance.spacing.small StyledText { Layout.alignment: Qt.AlignVCenter text: Time.hourStr color: Colours.palette.m3secondary font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) font.family: Appearance.font.family.clock font.bold: true } StyledText { Layout.alignment: Qt.AlignVCenter text: ":" color: Colours.palette.m3primary font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) font.family: Appearance.font.family.clock font.bold: true } StyledText { Layout.alignment: Qt.AlignVCenter text: Time.minuteStr color: Colours.palette.m3secondary font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) font.family: Appearance.font.family.clock font.bold: true } Loader { asynchronous: true Layout.leftMargin: Appearance.spacing.small Layout.alignment: Qt.AlignVCenter active: Config.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary font.pointSize: Math.floor(Appearance.font.size.extraLarge * 2 * root.centerScale) font.family: Appearance.font.family.clock font.bold: true } } } StyledText { Layout.alignment: Qt.AlignHCenter Layout.topMargin: -Appearance.padding.large * 2 text: Time.format("dddd, d MMMM yyyy") color: Colours.palette.m3tertiary font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) font.family: Appearance.font.family.mono font.bold: true } StyledClippingRect { Layout.topMargin: Appearance.spacing.large * 2 Layout.alignment: Qt.AlignHCenter implicitWidth: root.centerWidth / 2 implicitHeight: root.centerWidth / 2 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.full MaterialIcon { anchors.centerIn: parent text: "person" color: Colours.palette.m3onSurfaceVariant font.pointSize: Math.floor(root.centerWidth / 4) visible: pfp.status !== Image.Ready } CachingImage { id: pfp anchors.fill: parent path: `${Paths.home}/.face` } } StyledRect { Layout.alignment: Qt.AlignHCenter implicitWidth: root.centerWidth * 0.8 implicitHeight: input.implicitHeight + Appearance.padding.small * 2 color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.full focus: true onActiveFocusChanged: { if (!activeFocus) forceActiveFocus(); } Keys.onPressed: event => { if (root.lock.unlocking) return; if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) inputField.placeholder.animate = false; root.lock.pam.handleKey(event); } StateLayer { function onClicked(): void { parent.forceActiveFocus(); } hoverEnabled: false cursorShape: Qt.IBeamCursor } RowLayout { id: input anchors.fill: parent anchors.margins: Appearance.padding.small spacing: Appearance.spacing.normal Item { implicitWidth: implicitHeight implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 MaterialIcon { id: fprintIcon anchors.centerIn: parent animate: true text: { if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) return "fingerprint_off"; if (root.lock.pam.fprint.active) return "fingerprint"; return "lock"; } color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface opacity: root.lock.pam.passwd.active ? 0 : 1 Behavior on opacity { Anim {} } } CircularIndicator { anchors.fill: parent running: root.lock.pam.passwd.active } } InputField { id: inputField pam: root.lock.pam } StyledRect { implicitWidth: implicitHeight implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) radius: Appearance.rounding.full StateLayer { function onClicked(): void { root.lock.pam.passwd.start(); } color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { id: enterIcon anchors.centerIn: parent text: "arrow_forward" color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface font.weight: 500 } } } } Item { Layout.fillWidth: true Layout.topMargin: -Appearance.spacing.large implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) Behavior on implicitHeight { Anim {} } StyledText { id: stateMessage readonly property string msg: { if (Hypr.kbLayout !== Hypr.defaultKbLayout) { if (Hypr.capsLock && Hypr.numLock) return qsTr("Caps lock and Num lock are ON.\nKeyboard layout: %1").arg(Hypr.kbLayoutFull); if (Hypr.capsLock) return qsTr("Caps lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); if (Hypr.numLock) return qsTr("Num lock is ON. Kb layout: %1").arg(Hypr.kbLayoutFull); return qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull); } if (Hypr.capsLock && Hypr.numLock) return qsTr("Caps lock and Num lock are ON."); if (Hypr.capsLock) return qsTr("Caps lock is ON."); if (Hypr.numLock) return qsTr("Num lock is ON."); return ""; } property bool shouldBeVisible onMsgChanged: { if (msg) { if (opacity > 0) { animate = true; text = msg; animate = false; } else { text = msg; } shouldBeVisible = true; } else { shouldBeVisible = false; } } anchors.left: parent.left anchors.right: parent.right scale: shouldBeVisible && !message.msg ? 1 : 0.7 opacity: shouldBeVisible && !message.msg ? 1 : 0 color: Colours.palette.m3onSurfaceVariant animateProp: "opacity" font.family: Appearance.font.family.mono horizontalAlignment: Qt.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere lineHeight: 1.2 Behavior on scale { Anim {} } Behavior on opacity { Anim {} } } StyledText { id: message readonly property Pam pam: root.lock.pam readonly property string msg: { if (pam.fprintState === "error") return qsTr("FP ERROR: %1").arg(pam.fprint.message); if (pam.state === "error") return qsTr("PW ERROR: %1").arg(pam.passwd.message); if (pam.lockMessage) return pam.lockMessage; if (pam.state === "max" && pam.fprintState === "max") return qsTr("Maximum password and fingerprint attempts reached."); if (pam.state === "max") { if (pam.fprint.available) return qsTr("Maximum password attempts reached. Please use fingerprint."); return qsTr("Maximum password attempts reached."); } if (pam.fprintState === "max") return qsTr("Maximum fingerprint attempts reached. Please use password."); if (pam.state === "fail") { if (pam.fprint.available) return qsTr("Incorrect password. Please try again or use fingerprint."); return qsTr("Incorrect password. Please try again."); } if (pam.fprintState === "fail") return qsTr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(Config.lock.maxFprintTries); return ""; } anchors.left: parent.left anchors.right: parent.right scale: 0.7 opacity: 0 color: Colours.palette.m3error font.pointSize: Appearance.font.size.small font.family: Appearance.font.family.mono horizontalAlignment: Qt.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere onMsgChanged: { if (msg) { if (opacity > 0) { animate = true; text = msg; animate = false; exitAnim.stop(); if (scale < 1) appearAnim.restart(); else flashAnim.restart(); } else { text = msg; exitAnim.stop(); appearAnim.restart(); } } else { appearAnim.stop(); flashAnim.stop(); exitAnim.start(); } } Connections { function onFlashMsg(): void { exitAnim.stop(); if (message.scale < 1) appearAnim.restart(); else flashAnim.restart(); } target: root.lock.pam } Anim { id: appearAnim target: message properties: "scale,opacity" to: 1 onFinished: flashAnim.restart() } SequentialAnimation { id: flashAnim loops: 2 FlashAnim { to: 0.3 } FlashAnim { to: 1 } } ParallelAnimation { id: exitAnim Anim { target: message property: "scale" to: 0.7 duration: Appearance.anim.durations.large } Anim { target: message property: "opacity" to: 0 duration: Appearance.anim.durations.large } } } } component FlashAnim: NumberAnimation { target: message property: "opacity" duration: Appearance.anim.durations.small easing.type: Easing.Linear } } ================================================ FILE: modules/lock/Content.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts RowLayout { id: root required property var lock spacing: Appearance.spacing.large * 2 ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledRect { Layout.fillWidth: true implicitHeight: weather.implicitHeight topLeftRadius: Appearance.rounding.large radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainer WeatherInfo { id: weather rootHeight: root.height } } StyledRect { Layout.fillWidth: true Layout.fillHeight: true radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainer Fetch {} } StyledClippingRect { Layout.fillWidth: true implicitHeight: media.implicitHeight bottomLeftRadius: Appearance.rounding.large radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainer Media { id: media lock: root.lock } } } Center { lock: root.lock } ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal StyledRect { Layout.fillWidth: true implicitHeight: resources.implicitHeight topRightRadius: Appearance.rounding.large radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainer Resources { id: resources } } StyledRect { Layout.fillWidth: true Layout.fillHeight: true bottomRightRadius: Appearance.rounding.large radius: Appearance.rounding.small color: Colours.tPalette.m3surfaceContainer NotifDock { lock: root.lock } } } } ================================================ FILE: modules/lock/Fetch.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.effects import qs.services import qs.config import qs.utils import Quickshell.Services.UPower import QtQuick import QtQuick.Layouts ColumnLayout { id: root anchors.fill: parent anchors.margins: Appearance.padding.large * 2 anchors.topMargin: Appearance.padding.large spacing: Appearance.spacing.small RowLayout { Layout.fillWidth: true Layout.fillHeight: false spacing: Appearance.spacing.normal StyledRect { implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2 implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2 color: Colours.palette.m3primary radius: Appearance.rounding.small MonoText { id: prompt anchors.centerIn: parent text: ">" font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal color: Colours.palette.m3onPrimary } } MonoText { Layout.fillWidth: true text: "caelestiafetch.sh" font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal elide: Text.ElideRight } WrappedLoader { Layout.fillHeight: true active: !iconLoader.active sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } } RowLayout { Layout.fillWidth: true Layout.fillHeight: false spacing: height * 0.15 WrappedLoader { id: iconLoader Layout.fillHeight: true active: root.width > 320 sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } ColumnLayout { Layout.fillWidth: true Layout.topMargin: Appearance.padding.normal Layout.bottomMargin: Appearance.padding.normal Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 spacing: Appearance.spacing.normal WrappedLoader { Layout.fillWidth: true active: !batLoader.active && root.height > 200 sourceComponent: FetchText { text: `OS : ${SysInfo.osPrettyName || SysInfo.osName}` } } WrappedLoader { Layout.fillWidth: true active: root.height > (batLoader.active ? 200 : 110) sourceComponent: FetchText { text: `WM : ${SysInfo.wm}` } } WrappedLoader { Layout.fillWidth: true active: !batLoader.active || root.height > 110 sourceComponent: FetchText { text: `USER: ${SysInfo.user}` } } FetchText { text: `UP : ${SysInfo.uptime}` } WrappedLoader { id: batLoader Layout.fillWidth: true active: UPower.displayDevice.isLaptopBattery sourceComponent: FetchText { text: `BATT: ${[UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state) ? "(+) " : ""}${Math.round(UPower.displayDevice.percentage * 100)}%` } } } } WrappedLoader { Layout.alignment: Qt.AlignHCenter active: root.height > 180 sourceComponent: RowLayout { spacing: Appearance.spacing.large Repeater { model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large))) StyledRect { required property int index implicitWidth: implicitHeight implicitHeight: Appearance.font.size.larger * 2 color: Colours.palette[`term${index}`] radius: Appearance.rounding.small } } } } Component { id: caelestiaLogo Logo { width: height } } Component { id: distroIcon ColouredIcon { source: SysInfo.osLogo implicitSize: height colour: Colours.palette.m3primary layer.enabled: Config.lock.recolourLogo } } component WrappedLoader: Loader { asynchronous: true visible: active } component FetchText: MonoText { Layout.fillWidth: true font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal elide: Text.ElideRight } component MonoText: StyledText { font.family: Appearance.font.family.mono } } ================================================ FILE: modules/lock/InputField.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Layouts Item { id: root required property Pam pam readonly property alias placeholder: placeholder property string buffer Layout.fillWidth: true Layout.fillHeight: true clip: true Connections { function onBufferChanged(): void { if (root.pam.buffer.length > root.buffer.length) { charList.bindImWidth(); } else if (root.pam.buffer.length === 0) { charList.implicitWidth = charList.implicitWidth; placeholder.animate = true; } root.buffer = root.pam.buffer; } target: root.pam } StyledText { id: placeholder anchors.centerIn: parent text: { if (root.pam.passwd.active) return qsTr("Loading..."); if (root.pam.state === "max") return qsTr("You have reached the maximum number of tries"); return qsTr("Enter your password"); } animate: true color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline font.pointSize: Appearance.font.size.normal font.family: Appearance.font.family.mono opacity: root.buffer ? 0 : 1 Behavior on opacity { Anim {} } } ListView { id: charList readonly property int fullWidth: count * (implicitHeight + spacing) - spacing function bindImWidth(): void { imWidthBehavior.enabled = false; implicitWidth = Qt.binding(() => fullWidth); imWidthBehavior.enabled = true; } anchors.centerIn: parent anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 implicitWidth: fullWidth implicitHeight: Appearance.font.size.normal orientation: Qt.Horizontal spacing: Appearance.spacing.small / 2 interactive: false model: ScriptModel { values: root.buffer.split("") } delegate: StyledRect { id: ch implicitWidth: implicitHeight implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface radius: Appearance.rounding.small / 2 opacity: 0 scale: 0 Component.onCompleted: { opacity = 1; scale = 1; } ListView.onRemove: removeAnim.start() SequentialAnimation { id: removeAnim PropertyAction { target: ch property: "ListView.delayRemove" value: true } ParallelAnimation { Anim { target: ch property: "opacity" to: 0 } Anim { target: ch property: "scale" to: 0.5 } } PropertyAction { target: ch property: "ListView.delayRemove" value: false } } Behavior on opacity { Anim {} } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } Behavior on implicitWidth { id: imWidthBehavior Anim {} } } } ================================================ FILE: modules/lock/Lock.qml ================================================ pragma ComponentBehavior: Bound import qs.components.misc import Quickshell import Quickshell.Io import Quickshell.Wayland Scope { property alias lock: lock WlSessionLock { id: lock signal unlock LockSurface { lock: lock pam: pam } } Pam { id: pam lock: lock } CustomShortcut { name: "lock" description: "Lock the current session" onPressed: lock.locked = true } CustomShortcut { name: "unlock" description: "Unlock the current session" onPressed: lock.unlock() } IpcHandler { function lock(): void { lock.locked = true; } function unlock(): void { lock.unlock(); } function isLocked(): bool { return lock.locked; } target: "lock" } } ================================================ FILE: modules/lock/LockSurface.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell.Wayland import QtQuick import QtQuick.Effects WlSessionLockSurface { id: root required property WlSessionLock lock required property Pam pam readonly property alias unlocking: unlockAnim.running color: "transparent" Connections { function onUnlock(): void { unlockAnim.start(); } target: root.lock } SequentialAnimation { id: unlockAnim ParallelAnimation { Anim { target: lockContent properties: "implicitWidth,implicitHeight" to: lockContent.size duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } Anim { target: lockBg property: "radius" to: lockContent.radius } Anim { target: content property: "scale" to: 0 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } Anim { target: content property: "opacity" to: 0 duration: Appearance.anim.durations.small } Anim { target: lockIcon property: "opacity" to: 1 duration: Appearance.anim.durations.large } Anim { target: background property: "opacity" to: 0 duration: Appearance.anim.durations.large } SequentialAnimation { PauseAnimation { duration: Appearance.anim.durations.small } Anim { target: lockContent property: "opacity" to: 0 } } } PropertyAction { target: root.lock property: "locked" value: false } } ParallelAnimation { id: initAnim running: true Anim { target: background property: "opacity" to: 1 duration: Appearance.anim.durations.large } SequentialAnimation { ParallelAnimation { Anim { target: lockContent property: "scale" to: 1 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } Anim { target: lockContent property: "rotation" to: 360 duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.standardAccel } } ParallelAnimation { Anim { target: lockIcon property: "rotation" to: 360 easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { target: lockIcon property: "opacity" to: 0 } Anim { target: content property: "opacity" to: 1 } Anim { target: content property: "scale" to: 1 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } Anim { target: lockBg property: "radius" to: Appearance.rounding.large * 1.5 } Anim { target: lockContent property: "implicitWidth" to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } Anim { target: lockContent property: "implicitHeight" to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } ScreencopyView { id: background anchors.fill: parent captureSource: root.screen opacity: 0 layer.enabled: true layer.effect: MultiEffect { autoPaddingEnabled: false blurEnabled: true blur: 1 blurMax: 64 blurMultiplier: 1 } } Item { id: lockContent readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 readonly property int radius: size / 4 * Appearance.rounding.scale anchors.centerIn: parent implicitWidth: size implicitHeight: size rotation: 180 scale: 0 StyledRect { id: lockBg anchors.fill: parent color: Colours.palette.m3surface radius: parent.radius opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 layer.enabled: true layer.effect: MultiEffect { shadowEnabled: true blurMax: 15 shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) } } MaterialIcon { id: lockIcon anchors.centerIn: parent text: "lock" font.pointSize: Appearance.font.size.extraLarge * 4 font.bold: true rotation: 180 } Content { id: content anchors.centerIn: parent width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 lock: root opacity: 0 scale: 0 } } } ================================================ FILE: modules/lock/Media.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.effects import qs.services import qs.config import QtQuick import QtQuick.Layouts Item { id: root required property var lock anchors.left: parent.left anchors.right: parent.right implicitHeight: layout.implicitHeight Image { anchors.fill: parent source: Players.active?.trackArtUrl ?? "" asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height layer.enabled: true layer.effect: OpacityMask { maskSource: mask } opacity: status === Image.Ready ? 1 : 0 Behavior on opacity { Anim { duration: Appearance.anim.durations.extraLarge } } } Rectangle { id: mask anchors.fill: parent layer.enabled: true visible: false gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0 color: Qt.rgba(0, 0, 0, 0.5) } GradientStop { position: 0.4 color: Qt.rgba(0, 0, 0, 0.2) } GradientStop { position: 0.8 color: Qt.rgba(0, 0, 0, 0) } } } ColumnLayout { id: layout anchors.left: parent.left anchors.right: parent.right anchors.margins: Appearance.padding.large StyledText { Layout.topMargin: Appearance.padding.large Layout.bottomMargin: Appearance.spacing.larger text: qsTr("Now playing") color: Colours.palette.m3onSurfaceVariant font.family: Appearance.font.family.mono font.weight: 500 } StyledText { Layout.fillWidth: true animate: true text: Players.active?.trackArtist ?? qsTr("No media") color: Colours.palette.m3primary horizontalAlignment: Text.AlignHCenter font.pointSize: Appearance.font.size.large font.family: Appearance.font.family.mono font.weight: 600 elide: Text.ElideRight } StyledText { Layout.fillWidth: true animate: true text: Players.active?.trackTitle ?? qsTr("No media") horizontalAlignment: Text.AlignHCenter font.pointSize: Appearance.font.size.larger font.family: Appearance.font.family.mono elide: Text.ElideRight } RowLayout { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.large * 1.2 Layout.bottomMargin: Appearance.padding.large spacing: Appearance.spacing.large PlayerControl { function onClicked(): void { if (Players.active?.canGoPrevious) Players.active.previous(); } icon: "skip_previous" } PlayerControl { function onClicked(): void { if (Players.active?.canTogglePlaying) Players.active.togglePlaying(); } animate: true icon: active ? "pause" : "play_arrow" colour: "Primary" level: active ? 2 : 1 active: Players.active?.isPlaying ?? false } PlayerControl { function onClicked(): void { if (Players.active?.canGoNext) Players.active.next(); } icon: "skip_next" } } } component PlayerControl: StyledRect { id: control property alias animate: controlIcon.animate property alias icon: controlIcon.text property bool active property string colour: "Secondary" property int level: 1 function onClicked(): void { } Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2 color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`] radius: active || controlState.pressed ? Appearance.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Appearance.rounding.scale) Elevation { anchors.fill: parent radius: parent.radius z: -1 level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level } StateLayer { id: controlState function onClicked(): void { control.onClicked(); } color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] } MaterialIcon { id: controlIcon anchors.centerIn: parent color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] font.pointSize: Appearance.font.size.large fill: control.active ? 1 : 0 Behavior on fill { Anim {} } } Behavior on Layout.preferredWidth { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Behavior on radius { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } } ================================================ FILE: modules/lock/NotifDock.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property var lock anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.smaller StyledText { Layout.fillWidth: true text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline font.family: Appearance.font.family.mono font.weight: 500 elide: Text.ElideRight } ClippingRectangle { id: clipRect Layout.fillWidth: true Layout.fillHeight: true radius: Appearance.rounding.small color: "transparent" Loader { asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 sourceComponent: ColumnLayout { spacing: Appearance.spacing.large Image { asynchronous: true source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) fillMode: Image.PreserveAspectFit sourceSize.width: clipRect.width * 0.8 layer.enabled: true layer.effect: Colouriser { colorizationColor: Colours.palette.m3outlineVariant brightness: 1 } } StyledText { Layout.alignment: Qt.AlignHCenter text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") color: Colours.palette.m3outlineVariant font.pointSize: Appearance.font.size.large font.family: Appearance.font.family.mono font.weight: 500 } } Behavior on opacity { Anim { duration: Appearance.anim.durations.extraLarge } } } StyledListView { anchors.fill: parent visible: !Config.lock.hideNotifs spacing: Appearance.spacing.small clip: true model: ScriptModel { values: { const list = Notifs.notClosed.map(n => [n.appName, null]); return [...new Map(list).keys()]; } } delegate: NotifGroup {} add: Transition { Anim { property: "opacity" from: 0 to: 1 } Anim { property: "scale" from: 0 to: 1 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } remove: Transition { Anim { property: "opacity" to: 0 } Anim { property: "scale" to: 0.6 } } move: Transition { Anim { properties: "opacity,scale" to: 1 } Anim { property: "y" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } displaced: Transition { Anim { properties: "opacity,scale" to: 1 } Anim { property: "y" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } } ================================================ FILE: modules/lock/NotifGroup.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.effects import qs.services import qs.config import qs.utils import Quickshell import Quickshell.Widgets import Quickshell.Services.Notifications import QtQuick import QtQuick.Layouts StyledRect { id: root required property string modelData readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) readonly property var props: { let img = ""; let icon = ""; let hasCritical = false; let hasNormal = false; for (const n of notifs) { if (!img && n.image.length > 0) img = n.image; if (!icon && n.appIcon.length > 0) icon = n.appIcon; if (n.urgency === NotificationUrgency.Critical) hasCritical = true; else if (n.urgency === NotificationUrgency.Normal) hasNormal = true; } return { img, icon, urgency: hasCritical ? "critical" : hasNormal ? "normal" : "low" }; } readonly property string image: props.img readonly property string appIcon: props.icon readonly property string urgency: props.urgency property bool expanded anchors.left: parent?.left anchors.right: parent?.right implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 clip: true radius: Appearance.rounding.normal color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) RowLayout { id: content anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop implicitWidth: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image Component { id: imageComp Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop sourceSize.width: Config.notifs.sizes.image sourceSize.height: Config.notifs.sizes.image cache: false asynchronous: true width: Config.notifs.sizes.image height: Config.notifs.sizes.image } } Component { id: appIconComp ColouredIcon { implicitSize: Math.round(Config.notifs.sizes.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") } } Component { id: materialIconComp MaterialIcon { text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } ClippingRectangle { anchors.fill: parent color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) : Colours.palette.m3secondaryContainer radius: Appearance.rounding.full Loader { asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image sourceComponent: StyledRect { implicitWidth: Config.notifs.sizes.badge implicitHeight: Config.notifs.sizes.badge color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3secondaryContainer radius: Appearance.rounding.full ColouredIcon { anchors.centerIn: parent implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") } } } } ColumnLayout { Layout.topMargin: -Appearance.padding.small Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing) Layout.fillWidth: true spacing: Math.round(Appearance.spacing.small / 2) RowLayout { Layout.bottomMargin: -parent.spacing Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small elide: Text.ElideRight } StyledText { animate: true text: root.notifs[0]?.timeStr ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledRect { implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 implicitHeight: groupCount.implicitHeight + Appearance.padding.small color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) radius: Appearance.rounding.full opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0 Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 StateLayer { function onClicked(): void { root.expanded = !root.expanded; } color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { id: expandBtn anchors.centerIn: parent spacing: Appearance.spacing.small / 2 StyledText { id: groupCount Layout.leftMargin: Appearance.padding.small / 2 animate: true text: root.notifs.length color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.small } MaterialIcon { Layout.rightMargin: -Appearance.padding.small / 2 animate: true text: root.expanded ? "expand_less" : "expand_more" color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface } } Behavior on opacity { Anim {} } Behavior on Layout.preferredWidth { Anim {} } } } Repeater { model: ScriptModel { values: root.notifs.slice(0, Config.notifs.groupPreviewNum) } NotifLine { id: notif ParallelAnimation { running: true Anim { target: notif property: "opacity" from: 0 to: 1 } Anim { target: notif property: "scale" from: 0.7 to: 1 } Anim { target: notif.Layout property: "preferredHeight" from: 0 to: notif.implicitHeight } } ParallelAnimation { running: notif.modelData.closed onFinished: notif.modelData.unlock(notif) Anim { target: notif property: "opacity" to: 0 } Anim { target: notif property: "scale" to: 0.7 } Anim { target: notif.Layout property: "preferredHeight" to: 0 } } } } Loader { asynchronous: true Layout.fillWidth: true opacity: root.expanded ? 1 : 0 Layout.preferredHeight: root.expanded ? implicitHeight : 0 active: opacity > 0 sourceComponent: ColumnLayout { Repeater { model: ScriptModel { values: root.notifs.slice(Config.notifs.groupPreviewNum) } NotifLine {} } } Behavior on opacity { Anim {} } } } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } component NotifLine: StyledText { id: notifLine required property NotifData modelData Layout.fillWidth: true textFormat: Text.MarkdownText text: { const summary = modelData.summary.replace(/\n/g, " "); const body = modelData.body.replace(/\n/g, " "); const colour = root.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline; if (metrics.text === metrics.elidedText) return `${summary} ${body}`; const t = metrics.elidedText.length - 3; if (t < summary.length) return `${summary.slice(0, t)}...`; return `${summary} ${body.slice(0, t - summary.length)}...`; } color: root.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface Component.onCompleted: modelData.lock(this) Component.onDestruction: modelData.unlock(this) TextMetrics { id: metrics text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\n/g, " ") font.pointSize: notifLine.font.pointSize font.family: notifLine.font.family elideWidth: notifLine.width elide: Text.ElideRight } } } ================================================ FILE: modules/lock/Pam.qml ================================================ import qs.config import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Services.Pam import QtQuick Scope { id: root required property WlSessionLock lock readonly property alias passwd: passwd readonly property alias fprint: fprint property string lockMessage property string state property string fprintState property string buffer signal flashMsg function handleKey(event: KeyEvent): void { if (passwd.active || state === "max") return; if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { passwd.start(); } else if (event.key === Qt.Key_Backspace) { if (event.modifiers & Qt.ControlModifier) { buffer = ""; } else { buffer = buffer.slice(0, -1); } } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { // No illegal characters (you are insane if you use unicode in your password) buffer += event.text; } } PamContext { id: passwd config: "passwd" configDirectory: Quickshell.shellDir + "/assets/pam.d" onMessageChanged: { if (message.startsWith("The account is locked")) root.lockMessage = message; else if (root.lockMessage && message.endsWith(" left to unlock)")) root.lockMessage += "\n" + message; } onResponseRequiredChanged: { if (!responseRequired) return; respond(root.buffer); root.buffer = ""; } onCompleted: res => { if (res === PamResult.Success) return root.lock.unlock(); if (res === PamResult.Error) root.state = "error"; else if (res === PamResult.MaxTries) root.state = "max"; else if (res === PamResult.Failed) root.state = "fail"; root.flashMsg(); stateReset.restart(); } } PamContext { id: fprint property bool available property int tries property int errorTries function checkAvail(): void { if (!available || !Config.lock.enableFprint || !root.lock.secure) { abort(); return; } tries = 0; errorTries = 0; start(); } config: "fprint" configDirectory: Quickshell.shellDir + "/assets/pam.d" onCompleted: res => { if (!available) return; if (res === PamResult.Success) return root.lock.unlock(); if (res === PamResult.Error) { root.fprintState = "error"; errorTries++; if (errorTries < 5) { abort(); errorRetry.restart(); } } else if (res === PamResult.MaxTries) { // Isn't actually the real max tries as pam only reports completed // when max tries is reached. tries++; if (tries < Config.lock.maxFprintTries) { // Restart if not actually real max tries root.fprintState = "fail"; start(); } else { root.fprintState = "max"; abort(); } } root.flashMsg(); fprintStateReset.start(); } } Process { id: availProc command: ["sh", "-c", "fprintd-list $USER"] onExited: code => { fprint.available = code === 0; fprint.checkAvail(); } } Timer { id: errorRetry interval: 800 onTriggered: fprint.start() } Timer { id: stateReset interval: 4000 onTriggered: { if (root.state !== "max") root.state = ""; } } Timer { id: fprintStateReset interval: 4000 onTriggered: { root.fprintState = ""; fprint.errorTries = 0; } } Connections { function onSecureChanged(): void { if (root.lock.secure) { availProc.running = true; root.buffer = ""; root.state = ""; root.fprintState = ""; root.lockMessage = ""; } } function onUnlock(): void { fprint.abort(); } target: root.lock } Connections { function onEnableFprintChanged(): void { fprint.checkAvail(); } target: Config.lock } } ================================================ FILE: modules/lock/Resources.qml ================================================ import qs.components import qs.components.controls import qs.components.misc import qs.services import qs.config import QtQuick import QtQuick.Layouts GridLayout { id: root anchors.left: parent.left anchors.right: parent.right anchors.margins: Appearance.padding.large rowSpacing: Appearance.spacing.large columnSpacing: Appearance.spacing.large rows: 2 columns: 2 Ref { service: SystemUsage } Resource { Layout.topMargin: Appearance.padding.large icon: "memory" value: SystemUsage.cpuPerc colour: Colours.palette.m3primary } Resource { Layout.topMargin: Appearance.padding.large icon: "thermostat" value: Math.min(1, SystemUsage.cpuTemp / 90) colour: Colours.palette.m3secondary } Resource { Layout.bottomMargin: Appearance.padding.large icon: "memory_alt" value: SystemUsage.memPerc colour: Colours.palette.m3secondary } Resource { Layout.bottomMargin: Appearance.padding.large icon: "hard_disk" value: SystemUsage.storagePerc colour: Colours.palette.m3tertiary } component Resource: StyledRect { id: res required property string icon required property real value required property color colour Layout.fillWidth: true implicitHeight: width color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) radius: Appearance.rounding.large CircularProgress { id: circ anchors.fill: parent value: res.value padding: Appearance.padding.large * 3 fgColour: res.colour bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal } MaterialIcon { id: icon anchors.centerIn: parent text: res.icon color: res.colour font.pointSize: (circ.arcRadius * 0.7) || 1 font.weight: 600 } Behavior on value { Anim { duration: Appearance.anim.durations.large } } } } ================================================ FILE: modules/lock/WeatherInfo.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property int rootHeight anchors.left: parent.left anchors.right: parent.right anchors.margins: Appearance.padding.large * 2 spacing: Appearance.spacing.small Loader { asynchronous: true Layout.topMargin: Appearance.padding.large * 2 Layout.bottomMargin: -Appearance.padding.large Layout.alignment: Qt.AlignHCenter active: root.rootHeight > 610 visible: active sourceComponent: StyledText { text: qsTr("Weather") color: Colours.palette.m3primary font.pointSize: Appearance.font.size.extraLarge font.weight: 500 } } RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.large MaterialIcon { animate: true text: Weather.icon color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.extraLarge * 2.5 } ColumnLayout { spacing: Appearance.spacing.small StyledText { Layout.fillWidth: true animate: true text: Weather.description color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.large font.weight: 500 elide: Text.ElideRight } StyledText { Layout.fillWidth: true animate: true text: qsTr("Humidity: %1%").arg(Weather.humidity) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } } Loader { asynchronous: true Layout.rightMargin: Appearance.padding.smaller active: root.width > 400 visible: active sourceComponent: ColumnLayout { spacing: Appearance.spacing.small StyledText { Layout.fillWidth: true animate: true text: Weather.temp color: Colours.palette.m3primary horizontalAlignment: Text.AlignRight font.pointSize: Appearance.font.size.extraLarge font.weight: 500 elide: Text.ElideLeft } StyledText { Layout.fillWidth: true animate: true text: qsTr("Feels like: %1").arg(Weather.feelsLike) color: Colours.palette.m3outline horizontalAlignment: Text.AlignRight font.pointSize: Appearance.font.size.smaller elide: Text.ElideLeft } } } } Loader { id: forecastLoader asynchronous: true Layout.topMargin: Appearance.spacing.smaller Layout.bottomMargin: Appearance.padding.large * 2 Layout.fillWidth: true active: root.rootHeight > 820 visible: active sourceComponent: RowLayout { spacing: Appearance.spacing.large Repeater { model: { const forecast = Weather.hourlyForecast; const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5; if (!forecast) return Array.from({ length: count }, () => null); return forecast.slice(0, count); } ColumnLayout { id: forecastHour required property var modelData Layout.fillWidth: true spacing: Appearance.spacing.small StyledText { Layout.fillWidth: true text: { const hour = forecastHour.modelData?.hour ?? 0; return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`; } color: Colours.palette.m3outline horizontalAlignment: Text.AlignHCenter font.pointSize: Appearance.font.size.larger } MaterialIcon { Layout.alignment: Qt.AlignHCenter text: forecastHour.modelData?.icon ?? "cloud_alert" font.pointSize: Appearance.font.size.extraLarge * 1.5 font.weight: 500 } StyledText { Layout.alignment: Qt.AlignHCenter text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.larger } } } } } Timer { running: true triggeredOnStart: true repeat: true interval: 900000 // 15 minutes onTriggered: Weather.reload() } } ================================================ FILE: modules/notifications/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper required property var sidebar readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface PathLine { relativeX: -(root.wrapper.width + root.rounding) relativeY: 0 } PathArc { relativeX: root.rounding relativeY: root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) } PathLine { relativeX: 0 relativeY: root.wrapper.height - root.roundingY * 2 } PathArc { relativeX: root.sidebar.notifsRoundingX relativeY: root.roundingY radiusX: root.sidebar.notifsRoundingX radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } PathLine { relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width relativeY: 0 } PathArc { relativeX: root.rounding relativeY: root.rounding radiusX: root.rounding radiusY: root.rounding } Behavior on fillColor { CAnim {} } } ================================================ FILE: modules/notifications/Content.qml ================================================ import qs.components import qs.components.containers import qs.components.widgets import qs.services import qs.config import Quickshell import Quickshell.Widgets import QtQuick Item { id: root required property DrawerVisibilities visibilities required property Item osdPanel required property Item sessionPanel readonly property int padding: Appearance.padding.large anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right implicitWidth: Config.notifs.sizes.width + padding * 2 implicitHeight: { const count = list.count; if (count === 0) return 0; let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; if (visibilities.osd) { const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; if (height > h) height = h; } if (visibilities.session) { const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; if (height > h) height = h; } return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { anchors.fill: parent anchors.margins: root.padding color: "transparent" radius: Appearance.rounding.normal StyledListView { id: list model: ScriptModel { values: Notifs.popups.filter(n => !n.closed) } anchors.fill: parent orientation: Qt.Vertical spacing: 0 cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0 delegate: NotifWrapper {} move: Transition { Anim { property: "y" } } displaced: Transition { Anim { property: "y" } } ExtraIndicator { anchors.top: parent.top extra: { const count = list.count; if (count === 0) return 0; const scrollY = list.contentY; let height = 0; for (let i = 0; i < count; i++) { height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return i; } return count; } } ExtraIndicator { anchors.bottom: parent.bottom extra: { const count = list.count; if (count === 0) return 0; const scrollY = list.contentHeight - (list.contentY + list.height); let height = 0; for (let i = count - 1; i >= 0; i--) { height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return count - i - 1; } return 0; } } } } Behavior on implicitHeight { Anim {} } component NotifWrapper: Item { id: wrapper required property NotifData modelData required property int index readonly property alias nonAnimHeight: notif.nonAnimHeight property int idx onIndexChanged: { if (index !== -1) idx = index; } implicitWidth: notif.implicitWidth implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) ListView.onRemove: removeAnim.start() SequentialAnimation { id: removeAnim PropertyAction { target: wrapper property: "ListView.delayRemove" value: true } PropertyAction { target: wrapper property: "enabled" value: false } PropertyAction { target: wrapper property: "implicitHeight" value: 0 } PropertyAction { target: wrapper property: "z" value: 1 } Anim { target: notif property: "x" to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 duration: Appearance.anim.durations.normal easing.bezierCurve: Appearance.anim.curves.emphasized } PropertyAction { target: wrapper property: "ListView.delayRemove" value: false } } ClippingRectangle { anchors.top: parent.top anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller color: "transparent" radius: notif.radius implicitWidth: notif.implicitWidth implicitHeight: notif.implicitHeight Notification { id: notif modelData: wrapper.modelData } } } component Anim: NumberAnimation { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } ================================================ FILE: modules/notifications/Notification.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.effects import qs.services import qs.config import qs.utils import Quickshell import Quickshell.Widgets import Quickshell.Services.Notifications import QtQuick import QtQuick.Layouts import QtQuick.Shapes StyledRect { id: root required property NotifData modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 property bool expanded: Config.notifs.openExpanded color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight x: Config.notifs.sizes.width Component.onCompleted: { x = 0; modelData.lock(this); } Component.onDestruction: modelData.unlock(this) Behavior on x { Anim { easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } } MouseArea { property int startY anchors.fill: parent hoverEnabled: true cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined acceptedButtons: Qt.LeftButton | Qt.MiddleButton preventStealing: true onEntered: root.modelData.timer.stop() onExited: { if (!pressed) root.modelData.timer.start(); } drag.target: parent drag.axis: Drag.XAxis onPressed: event => { root.modelData.timer.stop(); startY = event.y; if (event.button === Qt.MiddleButton) root.modelData.close(); } onReleased: event => { if (!containsMouse) root.modelData.timer.start(); if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) root.x = 0; else root.modelData.popup = false; } onPositionChanged: event => { if (pressed) { const diffY = event.y - startY; if (Math.abs(diffY) > Config.notifs.expandThreshold) root.expanded = diffY > 0; } } onClicked: event => { if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) return; const actions = root.modelData.actions; if (actions?.length === 1) actions[0].invoke(); } Item { id: inner anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Appearance.padding.normal implicitHeight: root.nonAnimHeight Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Loader { id: image asynchronous: true active: root.hasImage anchors.left: parent.left anchors.top: parent.top width: Config.notifs.sizes.image height: Config.notifs.sizes.image visible: root.hasImage || root.hasAppIcon sourceComponent: ClippingRectangle { radius: Appearance.rounding.full implicitWidth: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image Image { anchors.fill: parent source: Qt.resolvedUrl(root.modelData.image) fillMode: Image.PreserveAspectCrop sourceSize.width: Config.notifs.sizes.image sourceSize.height: Config.notifs.sizes.image cache: false asynchronous: true } } } Loader { id: appIcon asynchronous: true active: root.hasAppIcon || !root.hasImage anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter anchors.right: root.hasImage ? image.right : undefined anchors.bottom: root.hasImage ? image.bottom : undefined sourceComponent: StyledRect { radius: Appearance.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image Loader { id: icon asynchronous: true active: root.hasAppIcon anchors.centerIn: parent width: Math.round(parent.width * 0.6) height: Math.round(parent.width * 0.6) sourceComponent: ColouredIcon { anchors.fill: parent source: Quickshell.iconPath(root.modelData.appIcon) colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.modelData.appIcon.endsWith("symbolic") } } Loader { asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } } } Shape { id: progressIndicator anchors.centerIn: appIcon width: appIcon.implicitWidth + progressShape.strokeWidth * 2 height: appIcon.implicitHeight + progressShape.strokeWidth * 2 preferredRendererType: Shape.CurveRenderer ShapePath { id: progressShape capStyle: ShapePath.RoundCap fillColor: "transparent" strokeWidth: 2 strokeColor: Colours.palette.m3primary PathAngleArc { id: progressArc radiusX: progressIndicator.width / 2 - Appearance.padding.small / 2 centerX: progressIndicator.width / 2 radiusY: progressIndicator.height / 2 - Appearance.padding.small / 2 centerY: progressIndicator.height / 2 startAngle: -90 sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360 Behavior on sweepAngle { Anim { easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } } } } } StyledText { id: appName anchors.top: parent.top anchors.left: image.right anchors.leftMargin: Appearance.spacing.smaller animate: true text: appNameMetrics.elidedText maximumLineCount: 1 color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small opacity: root.expanded ? 1 : 0 Behavior on opacity { Anim {} } } TextMetrics { id: appNameMetrics text: root.modelData.appName font.family: appName.font.family font.pointSize: appName.font.pointSize elide: Text.ElideRight elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 } StyledText { id: summary anchors.top: parent.top anchors.left: image.right anchors.leftMargin: Appearance.spacing.smaller animate: true text: summaryMetrics.elidedText maximumLineCount: 1 height: implicitHeight states: State { name: "expanded" when: root.expanded PropertyChanges { summary.maximumLineCount: undefined } AnchorChanges { target: summary anchors.top: appName.bottom } } transitions: Transition { PropertyAction { target: summary property: "maximumLineCount" } AnchorAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } } Behavior on height { Anim {} } } TextMetrics { id: summaryMetrics text: root.modelData.summary font.family: summary.font.family font.pointSize: summary.font.pointSize elide: Text.ElideRight elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 } StyledText { id: timeSep anchors.top: parent.top anchors.left: summary.right anchors.leftMargin: Appearance.spacing.small text: "•" color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small states: State { name: "expanded" when: root.expanded AnchorChanges { target: timeSep anchors.left: appName.right } } transitions: Transition { AnchorAnimation { duration: Appearance.anim.durations.normal easing.type: Easing.BezierSpline easing.bezierCurve: Appearance.anim.curves.standard } } } StyledText { id: time anchors.top: parent.top anchors.left: timeSep.right anchors.leftMargin: Appearance.spacing.small animate: true horizontalAlignment: Text.AlignLeft text: root.modelData.timeStr color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } Item { id: expandBtn anchors.right: parent.right anchors.top: parent.top implicitWidth: expandIcon.height implicitHeight: expandIcon.height StateLayer { function onClicked() { root.expanded = !root.expanded; } radius: Appearance.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { id: expandIcon anchors.centerIn: parent animate: true text: root.expanded ? "expand_less" : "expand_more" font.pointSize: Appearance.font.size.normal } } StyledText { id: bodyPreview anchors.left: summary.left anchors.right: expandBtn.left anchors.top: summary.bottom anchors.rightMargin: Appearance.spacing.small animate: true textFormat: root.bodyTextFormat text: bodyPreviewMetrics.elidedText color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small opacity: root.expanded ? 0 : 1 Behavior on opacity { Anim {} } } TextMetrics { id: bodyPreviewMetrics text: root.modelData.body font.family: bodyPreview.font.family font.pointSize: bodyPreview.font.pointSize elide: Text.ElideRight elideWidth: bodyPreview.width } StyledText { id: body anchors.left: summary.left anchors.right: expandBtn.left anchors.top: summary.bottom anchors.rightMargin: Appearance.spacing.small animate: true textFormat: root.bodyTextFormat text: root.modelData.body color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere height: text ? implicitHeight : 0 onLinkActivated: link => { if (!root.expanded) return; Quickshell.execDetached(["app2unit", "-O", "--", link]); root.modelData.popup = false; } opacity: root.expanded ? 1 : 0 Behavior on opacity { Anim {} } } RowLayout { id: actions anchors.horizontalCenter: parent.horizontalCenter anchors.top: body.bottom anchors.topMargin: Appearance.spacing.small spacing: Appearance.spacing.smaller opacity: root.expanded ? 1 : 0 Behavior on opacity { Anim {} } Action { modelData: QtObject { readonly property string text: qsTr("Close") function invoke(): void { root.modelData.close(); } } } Repeater { model: root.modelData.actions delegate: Component { Action {} } } } } } component Action: StyledRect { id: action required property var modelData radius: Appearance.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 implicitWidth: actionText.width + Appearance.padding.normal * 2 implicitHeight: actionText.height + Appearance.padding.small * 2 StateLayer { function onClicked(): void { action.modelData.invoke(); } radius: Appearance.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface } StyledText { id: actionText anchors.centerIn: parent text: actionTextMetrics.elidedText color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } TextMetrics { id: actionTextMetrics text: action.modelData.text font.family: actionText.font.family font.pointSize: actionText.font.pointSize elide: Text.ElideRight elideWidth: { const numActions = root.modelData.actions.length + 1; return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2; } } } } ================================================ FILE: modules/notifications/Wrapper.qml ================================================ import qs.components import qs.config import QtQuick Item { id: root required property DrawerVisibilities visibilities required property Item sidebarPanel property alias osdPanel: content.osdPanel property alias sessionPanel: content.sessionPanel visible: height > 0 implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) implicitHeight: content.implicitHeight states: State { name: "hidden" when: root.visibilities.sidebar && Config.sidebar.enabled PropertyChanges { root.implicitHeight: 0 } } transitions: Transition { Anim { target: root property: "implicitHeight" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Content { id: content visibilities: root.visibilities } } ================================================ FILE: modules/osd/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface PathArc { relativeX: -root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding } PathLine { relativeX: -(root.wrapper.width - root.roundingX * 2) relativeY: 0 } PathArc { relativeX: -root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: root.wrapper.height - root.rounding * 2 } PathArc { relativeX: root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding direction: PathArc.Counterclockwise } PathLine { relativeX: root.wrapper.width - root.roundingX * 2 relativeY: 0 } PathArc { relativeX: root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding } Behavior on fillColor { CAnim {} } } ================================================ FILE: modules/osd/Content.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import qs.utils import QtQuick import QtQuick.Layouts Item { id: root required property Brightness.Monitor monitor required property DrawerVisibilities visibilities required property real volume required property bool muted required property real sourceVolume required property bool sourceMuted required property real brightness implicitWidth: layout.implicitWidth + Appearance.padding.large * 2 implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 ColumnLayout { id: layout anchors.centerIn: parent spacing: Appearance.spacing.normal // Speaker volume CustomMouseArea { function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementVolume(); else if (event.angleDelta.y < 0) Audio.decrementVolume(); } implicitWidth: Config.osd.sizes.sliderWidth implicitHeight: Config.osd.sizes.sliderHeight FilledSlider { anchors.fill: parent icon: Icons.getVolumeIcon(value, root.muted) value: root.volume to: Config.services.maxVolume onMoved: Audio.setVolume(value) } } // Microphone volume WrappedLoader { shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) sourceComponent: CustomMouseArea { function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementSourceVolume(); else if (event.angleDelta.y < 0) Audio.decrementSourceVolume(); } implicitWidth: Config.osd.sizes.sliderWidth implicitHeight: Config.osd.sizes.sliderHeight FilledSlider { anchors.fill: parent icon: Icons.getMicVolumeIcon(value, root.sourceMuted) value: root.sourceVolume to: Config.services.maxVolume onMoved: Audio.setSourceVolume(value) } } } // Brightness WrappedLoader { shouldBeActive: Config.osd.enableBrightness sourceComponent: CustomMouseArea { function onWheel(event: WheelEvent) { const monitor = root.monitor; if (!monitor) return; if (event.angleDelta.y > 0) monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); else if (event.angleDelta.y < 0) monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); } implicitWidth: Config.osd.sizes.sliderWidth implicitHeight: Config.osd.sizes.sliderHeight FilledSlider { anchors.fill: parent icon: `brightness_${(Math.round(value * 6) + 1)}` value: root.brightness onMoved: root.monitor?.setBrightness(value) } } } } component WrappedLoader: Loader { required property bool shouldBeActive asynchronous: true Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 opacity: shouldBeActive ? 1 : 0 active: opacity > 0 visible: active Behavior on Layout.preferredHeight { Anim { easing.bezierCurve: Appearance.anim.curves.emphasized } } Behavior on opacity { Anim {} } } } ================================================ FILE: modules/osd/Wrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import QtQuick Item { id: root required property ShellScreen screen required property DrawerVisibilities visibilities property bool hovered readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) property real volume property bool muted property real sourceVolume property bool sourceMuted property real brightness function show(): void { visibilities.osd = true; timer.restart(); } Component.onCompleted: { volume = Audio.volume; muted = Audio.muted; sourceVolume = Audio.sourceVolume; sourceMuted = Audio.sourceMuted; brightness = root.monitor?.brightness ?? 0; } visible: width > 0 implicitWidth: 0 implicitHeight: content.implicitHeight states: State { name: "visible" when: root.shouldBeActive PropertyChanges { root.implicitWidth: content.implicitWidth } } transitions: [ Transition { from: "" to: "visible" Anim { target: root property: "implicitWidth" easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } }, Transition { from: "visible" to: "" Anim { target: root property: "implicitWidth" easing.bezierCurve: Appearance.anim.curves.emphasized } } ] Connections { function onMutedChanged(): void { root.show(); root.muted = Audio.muted; } function onVolumeChanged(): void { root.show(); root.volume = Audio.volume; } function onSourceMutedChanged(): void { root.show(); root.sourceMuted = Audio.sourceMuted; } function onSourceVolumeChanged(): void { root.show(); root.sourceVolume = Audio.sourceVolume; } target: Audio } Connections { function onBrightnessChanged(): void { root.show(); root.brightness = root.monitor?.brightness ?? 0; } target: root.monitor } Timer { id: timer interval: Config.osd.hideDelay onTriggered: { if (!root.hovered) root.visibilities.osd = false; } } Loader { id: content anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) sourceComponent: Content { monitor: root.monitor visibilities: root.visibilities volume: root.volume muted: root.muted sourceVolume: root.sourceVolume sourceMuted: root.sourceMuted brightness: root.brightness } } } ================================================ FILE: modules/session/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface PathArc { relativeX: -root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding } PathLine { relativeX: -(root.wrapper.width - root.roundingX * 2) relativeY: 0 } PathArc { relativeX: -root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: root.wrapper.height - root.rounding * 2 } PathArc { relativeX: root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding direction: PathArc.Counterclockwise } PathLine { relativeX: root.wrapper.width - root.roundingX * 2 relativeY: 0 } PathArc { relativeX: root.roundingX relativeY: root.rounding radiusX: Math.min(root.rounding, root.wrapper.width) radiusY: root.rounding } Behavior on fillColor { CAnim {} } } ================================================ FILE: modules/session/Content.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import qs.utils import Quickshell import QtQuick Column { id: root required property DrawerVisibilities visibilities padding: Appearance.padding.large spacing: Appearance.spacing.large SessionButton { id: logout icon: Config.session.icons.logout command: Config.session.commands.logout KeyNavigation.down: shutdown Component.onCompleted: forceActiveFocus() Connections { function onLauncherChanged(): void { if (!root.visibilities.launcher) logout.forceActiveFocus(); } target: root.visibilities } } SessionButton { id: shutdown icon: Config.session.icons.shutdown command: Config.session.commands.shutdown KeyNavigation.up: logout KeyNavigation.down: hibernate } AnimatedImage { width: Config.session.sizes.button height: Config.session.sizes.button sourceSize.width: width sourceSize.height: height playing: visible asynchronous: true speed: Appearance.anim.sessionGifSpeed source: Paths.absolutePath(Config.paths.sessionGif) } SessionButton { id: hibernate icon: Config.session.icons.hibernate command: Config.session.commands.hibernate KeyNavigation.up: shutdown KeyNavigation.down: reboot } SessionButton { id: reboot icon: Config.session.icons.reboot command: Config.session.commands.reboot KeyNavigation.up: hibernate } component SessionButton: StyledRect { id: button required property string icon required property list command implicitWidth: Config.session.sizes.button implicitHeight: Config.session.sizes.button radius: Appearance.rounding.large color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer Keys.onEnterPressed: Quickshell.execDetached(button.command) Keys.onReturnPressed: Quickshell.execDetached(button.command) Keys.onEscapePressed: root.visibilities.session = false Keys.onPressed: event => { if (!Config.session.vimKeybinds) return; if (event.modifiers & Qt.ControlModifier) { if (event.key === Qt.Key_J && KeyNavigation.down) { KeyNavigation.down.focus = true; event.accepted = true; } else if (event.key === Qt.Key_K && KeyNavigation.up) { KeyNavigation.up.focus = true; event.accepted = true; } } else if (event.key === Qt.Key_Tab && KeyNavigation.down) { KeyNavigation.down.focus = true; event.accepted = true; } else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { if (KeyNavigation.up) { KeyNavigation.up.focus = true; event.accepted = true; } } } StateLayer { function onClicked(): void { Quickshell.execDetached(button.command); } radius: parent.radius color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { anchors.centerIn: parent text: button.icon color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.extraLarge font.weight: 500 } } } ================================================ FILE: modules/session/Wrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import QtQuick Item { id: root required property DrawerVisibilities visibilities required property var panels readonly property real nonAnimWidth: content.implicitWidth visible: width > 0 implicitWidth: 0 implicitHeight: content.implicitHeight states: State { name: "visible" when: root.visibilities.session && Config.session.enabled PropertyChanges { root.implicitWidth: root.nonAnimWidth } } transitions: [ Transition { from: "" to: "visible" Anim { target: root property: "implicitWidth" easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } }, Transition { from: "visible" to: "" Anim { target: root property: "implicitWidth" easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized } } ] Loader { id: content anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible) sourceComponent: Content { visibilities: root.visibilities } } } ================================================ FILE: modules/sidebar/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper required property var panels readonly property real rounding: Config.border.rounding readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface PathLine { relativeX: -root.wrapper.width - root.notifsRoundingX relativeY: 0 } PathArc { relativeX: root.notifsRoundingX relativeY: root.rounding radiusX: root.notifsRoundingX radiusY: root.rounding } PathLine { relativeX: 0 relativeY: root.wrapper.height - root.rounding * 2 } PathArc { relativeX: -root.utilsRoundingX relativeY: root.rounding radiusX: root.utilsRoundingX radiusY: root.rounding } PathLine { relativeX: root.wrapper.width + root.utilsRoundingX relativeY: 0 } Behavior on fillColor { CAnim {} } } ================================================ FILE: modules/sidebar/Content.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Layouts Item { id: root required property Props props required property DrawerVisibilities visibilities ColumnLayout { id: layout anchors.fill: parent spacing: Appearance.spacing.normal StyledRect { Layout.fillWidth: true Layout.fillHeight: true radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainerLow NotifDock { props: root.props visibilities: root.visibilities } } StyledRect { Layout.topMargin: Appearance.padding.large - layout.spacing Layout.fillWidth: true implicitHeight: 1 color: Colours.tPalette.m3outlineVariant } } } ================================================ FILE: modules/sidebar/Notif.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import QtQuick import QtQuick.Layouts StyledRect { id: root required property NotifData modelData required property Props props required property bool expanded required property DrawerVisibilities visibilities readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight radius: Appearance.rounding.small color: { const c = root.modelData.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); return expanded ? c : Qt.alpha(c, 0); } states: State { name: "expanded" when: root.expanded PropertyChanges { summary.anchors.margins: Appearance.padding.normal dummySummary.anchors.margins: Appearance.padding.normal compactBody.anchors.margins: Appearance.padding.normal timeStr.anchors.margins: Appearance.padding.normal expandedContent.anchors.margins: Appearance.padding.normal summary.width: root.width - Appearance.padding.normal * 2 - timeStr.implicitWidth - Appearance.spacing.small summary.maximumLineCount: Number.MAX_SAFE_INTEGER } } transitions: Transition { Anim { properties: "margins,width,maximumLineCount" } } TextMetrics { id: summaryHeightMetrics font: summary.font text: " " // Use this height to prevent weird characters from changing the line height } StyledText { id: summary anchors.top: parent.top anchors.left: parent.left width: parent.width text: root.modelData.summary color: root.modelData.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 1 } StyledText { id: dummySummary anchors.top: parent.top anchors.left: parent.left visible: false text: root.modelData.summary } WrappedLoader { id: compactBody shouldBeActive: !root.expanded anchors.top: parent.top anchors.left: dummySummary.right anchors.right: parent.right anchors.leftMargin: Appearance.spacing.small sourceComponent: StyledText { text: root.modelData.body.replace(/\n/g, " ") color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline elide: Text.ElideRight } } WrappedLoader { id: timeStr shouldBeActive: root.expanded anchors.top: parent.top anchors.right: parent.right sourceComponent: StyledText { animate: true text: root.modelData.timeStr color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } } WrappedLoader { id: expandedContent shouldBeActive: root.expanded anchors.top: summary.bottom anchors.left: parent.left anchors.right: parent.right anchors.topMargin: Appearance.spacing.small / 2 sourceComponent: ExpandedBody {} } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } component ExpandedBody: ColumnLayout { readonly property alias body: bodyText spacing: Appearance.spacing.smaller StyledText { id: bodyText Layout.fillWidth: true textFormat: Text.MarkdownText text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline wrapMode: Text.WordWrap onLinkActivated: link => { Quickshell.execDetached(["app2unit", "-O", "--", link]); root.visibilities.sidebar = false; } } NotifActionList { notif: root.modelData } } component WrappedLoader: Loader { required property bool shouldBeActive asynchronous: true opacity: shouldBeActive ? 1 : 0 active: opacity > 0 Behavior on opacity { Anim {} } } } ================================================ FILE: modules/sidebar/NotifActionList.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts Item { id: root required property NotifData notif Layout.fillWidth: true implicitHeight: flickable.contentHeight layer.enabled: true layer.smooth: true layer.effect: OpacityMask { maskSource: gradientMask } Item { id: gradientMask anchors.fill: parent layer.enabled: true visible: false Rectangle { anchors.fill: parent gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0 color: Qt.rgba(0, 0, 0, 0) } GradientStop { position: 0.1 color: Qt.rgba(0, 0, 0, 1) } GradientStop { position: 0.9 color: Qt.rgba(0, 0, 0, 1) } GradientStop { position: 1 color: Qt.rgba(0, 0, 0, 0) } } } Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left implicitWidth: parent.width / 2 opacity: flickable.contentX > 0 ? 0 : 1 Behavior on opacity { Anim {} } } Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right implicitWidth: parent.width / 2 opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1 Behavior on opacity { Anim {} } } } StyledFlickable { id: flickable anchors.fill: parent contentWidth: Math.max(width, actionList.implicitWidth) contentHeight: actionList.implicitHeight RowLayout { id: actionList anchors.fill: parent spacing: Appearance.spacing.small Repeater { model: [ { isClose: true }, ...root.notif.actions, { isCopy: true } ] StyledRect { id: action required property var modelData Layout.fillWidth: true Layout.fillHeight: true implicitWidth: actionInner.implicitWidth + Appearance.padding.normal * 2 implicitHeight: actionInner.implicitHeight + Appearance.padding.small * 2 Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Appearance.padding.large : 0) radius: actionStateLayer.pressed ? Appearance.rounding.small / 2 : Appearance.rounding.small color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4) Timer { id: copyTimer interval: 3000 onTriggered: actionInner.item.text = "content_copy" } StateLayer { id: actionStateLayer function onClicked(): void { if (action.modelData.isClose) { root.notif.close(); } else if (action.modelData.isCopy) { Quickshell.clipboardText = root.notif.body; actionInner.item.text = "inventory"; copyTimer.start(); } else if (action.modelData.invoke) { action.modelData.invoke(); } else if (!root.notif.resident) { root.notif.close(); } } } Loader { id: actionInner anchors.centerIn: parent sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp } Component { id: iconBtn MaterialIcon { animate: action.modelData.isCopy ?? false text: action.modelData.isCopy ? "content_copy" : "close" color: Colours.palette.m3onSurfaceVariant } } Component { id: iconComp IconImage { asynchronous: true source: Quickshell.iconPath(action.modelData.identifier) } } Component { id: textComp StyledText { text: action.modelData.text color: Colours.palette.m3onSurfaceVariant } } Behavior on Layout.preferredWidth { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Behavior on radius { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } } } } } ================================================ FILE: modules/sidebar/NotifDock.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.components.containers import qs.components.effects import qs.services import qs.config import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts Item { id: root required property Props props required property DrawerVisibilities visibilities readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent anchors.margins: Appearance.padding.normal Component.onCompleted: Notifs.list.forEach(n => n.popup = false) Item { id: title anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: Appearance.padding.small implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) StyledText { id: count anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin opacity: root.notifCount > 0 ? 1 : 0 text: root.notifCount color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal font.family: Appearance.font.family.mono font.weight: 500 Behavior on anchors.leftMargin { Anim {} } Behavior on opacity { Anim {} } } StyledText { id: titleText anchors.verticalCenter: parent.verticalCenter anchors.left: count.right anchors.right: parent.right anchors.leftMargin: Appearance.spacing.small text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal font.family: Appearance.font.family.mono font.weight: 500 elide: Text.ElideRight } } ClippingRectangle { id: clipRect anchors.left: parent.left anchors.right: parent.right anchors.top: title.bottom anchors.bottom: parent.bottom anchors.topMargin: Appearance.spacing.smaller radius: Appearance.rounding.small color: "transparent" Loader { asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: root.notifCount > 0 ? 0 : 1 sourceComponent: ColumnLayout { spacing: Appearance.spacing.large Image { asynchronous: true source: Quickshell.shellPath("assets/dino.png") fillMode: Image.PreserveAspectFit sourceSize.width: clipRect.width * 0.8 layer.enabled: true layer.effect: Colouriser { colorizationColor: Colours.palette.m3outlineVariant brightness: 1 } } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No Notifications") color: Colours.palette.m3outlineVariant font.pointSize: Appearance.font.size.large font.family: Appearance.font.family.mono font.weight: 500 } } Behavior on opacity { Anim { duration: Appearance.anim.durations.extraLarge } } } StyledFlickable { id: view anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentWidth: width contentHeight: notifList.implicitHeight StyledScrollBar.vertical: StyledScrollBar { flickable: view } NotifDockList { id: notifList props: root.props visibilities: root.visibilities container: view } } } Timer { id: clearTimer repeat: true interval: 50 onTriggered: { let next = null; for (let i = 0; i < notifList.repeater.count; i++) { next = notifList.repeater.itemAt(i); if (!next?.closed) // qmllint disable missing-property break; } if (next) { next.closeAll(); // qmllint disable missing-property } else { stop(); } } } Loader { asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Appearance.padding.normal scale: root.notifCount > 0 ? 1 : 0.5 opacity: root.notifCount > 0 ? 1 : 0 active: opacity > 0 sourceComponent: IconButton { id: clearBtn icon: "clear_all" radius: Appearance.rounding.normal padding: Appearance.padding.normal font.pointSize: Math.round(Appearance.font.size.large * 1.2) onClicked: clearTimer.start() Elevation { anchors.fill: parent radius: parent.radius z: -1 level: clearBtn.stateLayer.containsMouse ? 4 : 3 } } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } Behavior on opacity { Anim { duration: Appearance.anim.durations.expressiveFastSpatial } } } } ================================================ FILE: modules/sidebar/NotifDockList.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import QtQuick Item { id: root required property Props props required property Flickable container required property DrawerVisibilities visibilities readonly property alias repeater: repeater readonly property int spacing: Appearance.spacing.small property bool flag anchors.left: parent.left anchors.right: parent.right implicitHeight: { const item = repeater.itemAt(repeater.count - 1); return item ? item.y + item.implicitHeight : 0; } Repeater { id: repeater model: ScriptModel { values: { const map = new Map(); for (const n of Notifs.notClosed) map.set(n.appName, null); for (const n of Notifs.list) map.set(n.appName, null); return [...map.keys()]; } onValuesChanged: root.flagChanged() } delegate: NotifGroupDelegate {} } component NotifGroupDelegate: MouseArea { id: notif required property int index required property string modelData readonly property bool closed: notifInner.notifCount === 0 readonly property alias nonAnimHeight: notifInner.nonAnimHeight property int startY function closeAll(): void { for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) n.close(); } y: { root.flag; // Force update let y = 0; for (let i = 0; i < index; i++) { const item = repeater.itemAt(i) as NotifGroupDelegate; if (item && !item.closed) y += item.nonAnimHeight + root.spacing; } return y; } containmentMask: QtObject { function contains(p: point): bool { if (!root.container.contains(notif.mapToItem(root.container, p))) return false; return notifInner.contains(p); } } implicitWidth: root.width implicitHeight: notifInner.implicitHeight hoverEnabled: true cursorShape: pressed ? Qt.ClosedHandCursor : undefined acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton preventStealing: true enabled: !closed drag.target: this drag.axis: Drag.XAxis onPressed: event => { startY = event.y; if (event.button === Qt.RightButton) notifInner.toggleExpand(!notifInner.expanded); else if (event.button === Qt.MiddleButton) closeAll(); } onPositionChanged: event => { if (pressed) { const diffY = event.y - startY; if (Math.abs(diffY) > Config.notifs.expandThreshold) notifInner.toggleExpand(diffY > 0); } } onReleased: event => { if (Math.abs(x) < width * Config.notifs.clearThreshold) x = 0; else closeAll(); } ParallelAnimation { running: true Anim { target: notif property: "opacity" from: 0 to: 1 } Anim { target: notif property: "scale" from: 0 to: 1 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } ParallelAnimation { running: notif.closed Anim { target: notif property: "opacity" to: 0 } Anim { target: notif property: "scale" to: 0.6 } } NotifGroup { id: notifInner modelData: notif.modelData props: root.props container: root.container visibilities: root.visibilities } Behavior on x { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Behavior on y { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } ================================================ FILE: modules/sidebar/NotifGroup.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.effects import qs.services import qs.config import qs.utils import Quickshell import Quickshell.Services.Notifications import QtQuick import QtQuick.Layouts StyledRect { id: root required property string modelData required property Props props required property Flickable container required property DrawerVisibilities visibilities readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) readonly property var groupProps: { let count = 0; let img = ""; let icon = ""; let hasCritical = false; let hasNormal = false; for (const n of notifs) { if (!n.closed) { count++; if (!img && n.image.length > 0) img = n.image; if (!icon && n.appIcon.length > 0) icon = n.appIcon; if (n.urgency === NotificationUrgency.Critical) hasCritical = true; else if (n.urgency === NotificationUrgency.Normal) hasNormal = true; } } return { count, img, icon, urgency: hasCritical ? NotificationUrgency.Critical : hasNormal ? NotificationUrgency.Normal : NotificationUrgency.Low }; } readonly property int notifCount: groupProps.count readonly property string image: groupProps.img readonly property string appIcon: groupProps.icon readonly property int urgency: groupProps.urgency readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0); const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) function toggleExpand(expand: bool): void { if (expand) { if (!expanded) props.expandedNotifs.push(modelData); } else if (expanded) { props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); } } Component.onDestruction: { if (notifCount === 0 && expanded) props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); } anchors.left: parent?.left anchors.right: parent?.right implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 clip: true radius: Appearance.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) RowLayout { id: content anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Appearance.padding.normal spacing: Appearance.spacing.normal Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop implicitWidth: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image Component { id: imageComp Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop sourceSize.width: Config.notifs.sizes.image sourceSize.height: Config.notifs.sizes.image cache: false asynchronous: true width: Config.notifs.sizes.image height: Config.notifs.sizes.image } } Component { id: appIconComp ColouredIcon { implicitSize: Math.round(Config.notifs.sizes.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") } } Component { id: materialIconComp MaterialIcon { text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } StyledClippingRect { anchors.fill: parent color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) : Colours.palette.m3secondaryContainer radius: Appearance.rounding.full Loader { asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image sourceComponent: StyledRect { implicitWidth: Config.notifs.sizes.badge implicitHeight: Config.notifs.sizes.badge color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer radius: Appearance.rounding.full ColouredIcon { anchors.centerIn: parent implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") } } } } ColumnLayout { id: column Layout.topMargin: -Appearance.padding.small Layout.bottomMargin: -Appearance.padding.small / 2 Layout.fillWidth: true spacing: 0 RowLayout { id: header Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0 Layout.fillWidth: true spacing: Appearance.spacing.smaller StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small elide: Text.ElideRight } StyledText { animate: true text: root.notifs.find(n => !n.closed)?.timeStr ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } StyledRect { implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 implicitHeight: groupCount.implicitHeight + Appearance.padding.small color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) radius: Appearance.rounding.full StateLayer { function onClicked(): void { root.toggleExpand(!root.expanded); } color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { id: expandBtn anchors.centerIn: parent spacing: Appearance.spacing.small / 2 StyledText { id: groupCount Layout.leftMargin: Appearance.padding.small / 2 animate: true text: root.notifCount color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.small } MaterialIcon { Layout.rightMargin: -Appearance.padding.small / 2 text: "expand_more" color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface rotation: root.expanded ? 180 : 0 Layout.topMargin: root.expanded ? -Math.floor(Appearance.padding.smaller / 2) : 0 Behavior on rotation { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Behavior on Layout.topMargin { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } } Behavior on Layout.bottomMargin { Anim {} } } NotifGroupList { id: notifList props: root.props notifs: root.notifs expanded: root.expanded container: root.container visibilities: root.visibilities onRequestToggleExpand: expand => root.toggleExpand(expand) } } } } ================================================ FILE: modules/sidebar/NotifGroupList.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import qs.services import Quickshell import QtQuick import QtQuick.Layouts Item { id: root required property Props props required property list notifs required property bool expanded required property Flickable container required property DrawerVisibilities visibilities readonly property real nonAnimHeight: { let h = -root.spacing; for (let i = 0; i < repeater.count; i++) { const item = repeater.itemAt(i) as NotifDelegate; if (item && !item.modelData.closed && !item.previewHidden) h += item.nonAnimHeight + root.spacing; } return h; } readonly property int spacing: Math.round(Appearance.spacing.small / 2) property bool showAllNotifs property bool flag signal requestToggleExpand(expand: bool) onExpandedChanged: { if (expanded) { clearTimer.stop(); showAllNotifs = true; } else { clearTimer.start(); } } Layout.fillWidth: true implicitHeight: nonAnimHeight Timer { id: clearTimer interval: Appearance.anim.durations.normal onTriggered: root.showAllNotifs = false } Repeater { id: repeater model: ScriptModel { values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) onValuesChanged: root.flagChanged() } delegate: NotifDelegate {} } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } component NotifDelegate: MouseArea { id: notif required property int index required property NotifData modelData readonly property alias nonAnimHeight: notifInner.nonAnimHeight readonly property bool previewHidden: { if (root.expanded) return false; let extraHidden = 0; for (let i = 0; i < index; i++) if (root.notifs[i].closed) extraHidden++; return index >= Config.notifs.groupPreviewNum + extraHidden; } property int startY y: { root.flag; // Force update let y = 0; for (let i = 0; i < index; i++) { const item = repeater.itemAt(i) as NotifDelegate; if (item && !item.modelData.closed && !item.previewHidden) y += item.nonAnimHeight + root.spacing; } return y; } containmentMask: QtObject { function contains(p: point): bool { if (!root.container.contains(notif.mapToItem(root.container, p))) return false; return notifInner.contains(p); } } opacity: previewHidden ? 0 : 1 scale: previewHidden ? 0.7 : 1 implicitWidth: root.width implicitHeight: notifInner.implicitHeight hoverEnabled: true cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton preventStealing: !root.expanded enabled: !modelData.closed drag.target: this drag.axis: Drag.XAxis onPressed: event => { startY = event.y; if (event.button === Qt.RightButton) root.requestToggleExpand(!root.expanded); else if (event.button === Qt.MiddleButton) modelData.close(); } onPositionChanged: event => { if (pressed && !root.expanded) { const diffY = event.y - startY; if (Math.abs(diffY) > Config.notifs.expandThreshold) root.requestToggleExpand(diffY > 0); } } onReleased: event => { if (Math.abs(x) < width * Config.notifs.clearThreshold) x = 0; else modelData.close(); } Component.onCompleted: modelData.lock(this) Component.onDestruction: modelData.unlock(this) ParallelAnimation { Component.onCompleted: running = !notif.previewHidden Anim { target: notif property: "opacity" from: 0 to: 1 } Anim { target: notif property: "scale" from: 0.7 to: 1 } } ParallelAnimation { running: notif.modelData.closed onFinished: notif.modelData.unlock(notif) Anim { target: notif property: "opacity" to: 0 } Anim { target: notif property: "x" to: notif.x >= 0 ? notif.width : -notif.width } } Notif { id: notifInner anchors.fill: parent modelData: notif.modelData props: root.props expanded: root.expanded visibilities: root.visibilities } Behavior on opacity { Anim {} } Behavior on scale { Anim {} } Behavior on x { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Behavior on y { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } ================================================ FILE: modules/sidebar/Props.qml ================================================ import Quickshell PersistentProperties { property list expandedNotifs: [] reloadableId: "sidebar" } ================================================ FILE: modules/sidebar/Wrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import QtQuick Item { id: root required property DrawerVisibilities visibilities required property var panels readonly property Props props: Props {} visible: width > 0 implicitWidth: 0 states: State { name: "visible" when: root.visibilities.sidebar && Config.sidebar.enabled PropertyChanges { root.implicitWidth: Config.sidebar.sizes.width } } transitions: [ Transition { from: "" to: "visible" Anim { target: root property: "implicitWidth" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } }, Transition { from: "visible" to: "" Anim { target: root property: "implicitWidth" easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized } } ] Loader { id: content anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.margins: Appearance.padding.large anchors.bottomMargin: 0 active: true Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) sourceComponent: Content { implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2 props: root.props visibilities: root.visibilities } } } ================================================ FILE: modules/utilities/Background.qml ================================================ import qs.components import qs.services import qs.config import QtQuick import QtQuick.Shapes ShapePath { id: root required property Wrapper wrapper required property var sidebar readonly property real rounding: Config.border.rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding strokeWidth: -1 fillColor: Colours.palette.m3surface PathLine { relativeX: -(root.wrapper.width + root.rounding) relativeY: 0 } PathArc { relativeX: root.rounding relativeY: -root.roundingY radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: -(root.wrapper.height - root.roundingY * 2) } PathArc { relativeX: root.sidebar.utilsRoundingX relativeY: -root.roundingY radiusX: root.sidebar.utilsRoundingX radiusY: Math.min(root.rounding, root.wrapper.height) } PathLine { relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.utilsRoundingX : root.wrapper.width relativeY: 0 } PathArc { relativeX: root.rounding relativeY: -root.rounding radiusX: root.rounding radiusY: root.rounding direction: PathArc.Counterclockwise } Behavior on fillColor { CAnim {} } } ================================================ FILE: modules/utilities/Content.qml ================================================ import "cards" import qs.components import qs.config import qs.modules.bar.popouts as BarPopouts import QtQuick import QtQuick.Layouts Item { id: root required property var props required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight ColumnLayout { id: layout anchors.fill: parent spacing: Appearance.spacing.normal IdleInhibit {} Record { props: root.props visibilities: root.visibilities z: 1 } Toggles { visibilities: root.visibilities popouts: root.popouts } } RecordingDeleteModal { props: root.props } } ================================================ FILE: modules/utilities/RecordingDeleteModal.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config import Caelestia import QtQuick import QtQuick.Layouts import QtQuick.Shapes Loader { id: root required property var props asynchronous: true anchors.fill: parent opacity: root.props.recordingConfirmDelete ? 1 : 0 active: opacity > 0 sourceComponent: MouseArea { id: deleteConfirmation property string path Component.onCompleted: path = root.props.recordingConfirmDelete hoverEnabled: true onClicked: root.props.recordingConfirmDelete = "" Item { anchors.fill: parent anchors.margins: -Appearance.padding.large anchors.rightMargin: -Appearance.padding.large - Config.border.thickness anchors.bottomMargin: -Appearance.padding.large - Config.border.thickness opacity: 0.5 StyledRect { anchors.fill: parent topLeftRadius: Config.border.rounding color: Colours.palette.m3scrim } Shape { id: shape anchors.fill: parent preferredRendererType: Shape.CurveRenderer asynchronous: true ShapePath { startX: -Config.border.rounding * 2 startY: shape.height - Config.border.thickness strokeWidth: 0 fillGradient: LinearGradient { orientation: LinearGradient.Horizontal x1: -Config.border.rounding * 2 GradientStop { position: 0 color: Qt.alpha(Colours.palette.m3scrim, 0) } GradientStop { position: 1 color: Colours.palette.m3scrim } } PathLine { relativeX: Config.border.rounding relativeY: 0 } PathArc { relativeY: -Config.border.rounding radiusX: Config.border.rounding radiusY: Config.border.rounding direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: Config.border.rounding + Config.border.thickness } PathLine { relativeX: -Config.border.rounding * 2 relativeY: 0 } } ShapePath { startX: shape.width - Config.border.rounding - Config.border.thickness strokeWidth: 0 fillGradient: LinearGradient { orientation: LinearGradient.Vertical y1: -Config.border.rounding * 2 GradientStop { position: 0 color: Qt.alpha(Colours.palette.m3scrim, 0) } GradientStop { position: 1 color: Colours.palette.m3scrim } } PathArc { relativeX: Config.border.rounding relativeY: -Config.border.rounding radiusX: Config.border.rounding radiusY: Config.border.rounding direction: PathArc.Counterclockwise } PathLine { relativeX: 0 relativeY: -Config.border.rounding } PathLine { relativeX: Config.border.thickness relativeY: 0 } PathLine { relativeX: 0 } } } } StyledRect { anchors.centerIn: parent radius: Appearance.rounding.large color: Colours.palette.m3surfaceContainerHigh scale: 0 Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0) width: Math.min(parent.width - Appearance.padding.large * 2, implicitWidth) implicitWidth: deleteConfirmationLayout.implicitWidth + Appearance.padding.large * 3 implicitHeight: deleteConfirmationLayout.implicitHeight + Appearance.padding.large * 3 MouseArea { anchors.fill: parent } Elevation { anchors.fill: parent radius: parent.radius z: -1 level: 3 } ColumnLayout { id: deleteConfirmationLayout anchors.fill: parent anchors.margins: Appearance.padding.large * 1.5 spacing: Appearance.spacing.normal StyledText { text: qsTr("Delete recording?") font.pointSize: Appearance.font.size.large } StyledText { Layout.fillWidth: true text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere } RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.alignment: Qt.AlignRight spacing: Appearance.spacing.normal TextButton { text: qsTr("Cancel") type: TextButton.Text onClicked: root.props.recordingConfirmDelete = "" } TextButton { text: qsTr("Delete") type: TextButton.Text onClicked: { CUtils.deleteFile(Qt.resolvedUrl(root.props.recordingConfirmDelete)); root.props.recordingConfirmDelete = ""; } } } } Behavior on scale { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } Behavior on opacity { Anim {} } } ================================================ FILE: modules/utilities/Wrapper.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import Quickshell import QtQuick Item { id: root required property DrawerVisibilities visibilities required property Item sidebar required property Item popouts readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false property string recordingConfirmDelete property string recordingMode reloadableId: "utilities" } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) visible: height > 0 implicitHeight: 0 implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width onStateChanged: { if (state === "visible" && timer.running) { timer.triggered(); timer.stop(); } } states: State { name: "visible" when: root.shouldBeActive PropertyChanges { root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2 } } transitions: [ Transition { from: "" to: "visible" Anim { target: root property: "implicitHeight" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } }, Transition { from: "visible" to: "" Anim { target: root property: "implicitHeight" easing.bezierCurve: Appearance.anim.curves.emphasized } } ] Timer { id: timer running: true interval: Appearance.anim.durations.extraLarge onTriggered: { content.active = Qt.binding(() => root.shouldBeActive || root.visible); content.visible = true; } } Loader { id: content asynchronous: true anchors.top: parent.top anchors.left: parent.left anchors.margins: Appearance.padding.large visible: false active: true sourceComponent: Content { implicitWidth: root.implicitWidth - Appearance.padding.large * 2 props: root.props visibilities: root.visibilities popouts: root.popouts } } } ================================================ FILE: modules/utilities/cards/IdleInhibit.qml ================================================ import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root Layout.fillWidth: true implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer clip: true RowLayout { id: layout anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledRect { implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { id: icon anchors.centerIn: parent text: "coffee" color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { Layout.fillWidth: true text: qsTr("Keep Awake") font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } StyledText { Layout.fillWidth: true text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small elide: Text.ElideRight } } StyledSwitch { checked: IdleInhibitor.enabled onToggled: IdleInhibitor.enabled = checked } } Loader { id: activeChip asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left anchors.topMargin: Appearance.spacing.larger anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight anchors.leftMargin: Appearance.padding.large opacity: IdleInhibitor.enabled ? 1 : 0 scale: IdleInhibitor.enabled ? 1 : 0.5 Component.onCompleted: active = Qt.binding(() => opacity > 0) sourceComponent: StyledRect { implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2 implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2 radius: Appearance.rounding.full color: Colours.palette.m3primary StyledText { id: activeText anchors.centerIn: parent text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) color: Colours.palette.m3onPrimary font.pointSize: Math.round(Appearance.font.size.small * 0.9) } } Behavior on anchors.bottomMargin { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Behavior on opacity { Anim { duration: Appearance.anim.durations.small } } Behavior on scale { Anim {} } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } ================================================ FILE: modules/utilities/cards/Record.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import QtQuick import QtQuick.Layouts StyledRect { id: root required property var props required property DrawerVisibilities visibilities Layout.fillWidth: true implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: layout anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal RowLayout { spacing: Appearance.spacing.normal z: 1 StyledRect { implicitWidth: implicitHeight implicitHeight: { const h = icon.implicitHeight + Appearance.padding.smaller * 2; return h - (h % 2); } radius: Appearance.rounding.full color: Recorder.running ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { id: icon anchors.centerIn: parent anchors.horizontalCenterOffset: -0.5 anchors.verticalCenterOffset: 1.5 text: "screen_record" color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { Layout.fillWidth: true text: qsTr("Screen Recorder") font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } StyledText { Layout.fillWidth: true text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off") color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small elide: Text.ElideRight } } SplitButton { disabled: Recorder.running active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0] menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text menuItems: [ MenuItem { icon: "fullscreen" text: qsTr("Record fullscreen") activeText: qsTr("Fullscreen") onClicked: Recorder.start() }, MenuItem { icon: "screenshot_region" text: qsTr("Record region") activeText: qsTr("Region") onClicked: Recorder.start(["-r"]) }, MenuItem { icon: "select_to_speak" text: qsTr("Record fullscreen with sound") activeText: qsTr("Fullscreen") onClicked: Recorder.start(["-s"]) }, MenuItem { icon: "volume_up" text: qsTr("Record region with sound") activeText: qsTr("Region") onClicked: Recorder.start(["-sr"]) } ] } } Loader { id: listOrControls property bool running: Recorder.running asynchronous: true Layout.fillWidth: true Layout.preferredHeight: implicitHeight sourceComponent: running ? recordingControls : recordingList Behavior on Layout.preferredHeight { id: locHeightAnim enabled: false Anim {} } Behavior on running { SequentialAnimation { ParallelAnimation { Anim { target: listOrControls property: "scale" to: 0.7 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardAccel } Anim { target: listOrControls property: "opacity" to: 0 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardAccel } } PropertyAction { target: locHeightAnim property: "enabled" value: true } PropertyAction {} PropertyAction { target: locHeightAnim property: "enabled" value: false } ParallelAnimation { Anim { target: listOrControls property: "scale" to: 1 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardDecel } Anim { target: listOrControls property: "opacity" to: 1 duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.standardDecel } } } } } } Component { id: recordingList RecordingList { props: root.props visibilities: root.visibilities } } Component { id: recordingControls RowLayout { spacing: Appearance.spacing.normal StyledRect { radius: Appearance.rounding.full color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2 implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2 StyledText { id: recText anchors.centerIn: parent animate: true text: Recorder.paused ? "PAUSED" : "REC" color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError font.family: Appearance.font.family.mono } Behavior on implicitWidth { Anim {} } SequentialAnimation on opacity { running: !Recorder.paused alwaysRunToEnd: true loops: Animation.Infinite Anim { from: 1 to: 0 duration: Appearance.anim.durations.large easing.bezierCurve: Appearance.anim.curves.emphasizedAccel } Anim { from: 0 to: 1 duration: Appearance.anim.durations.extraLarge easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } } } StyledText { text: { const elapsed = Recorder.elapsed; const hours = Math.floor(elapsed / 3600); const mins = Math.floor((elapsed % 3600) / 60); const secs = Math.floor(elapsed % 60).toString().padStart(2, "0"); let time; if (hours > 0) time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; else time = `${mins}:${secs}`; return qsTr("Recording for %1").arg(time); } font.pointSize: Appearance.font.size.normal } Item { Layout.fillWidth: true } IconButton { label.animate: true icon: Recorder.paused ? "play_arrow" : "pause" toggle: true checked: Recorder.paused type: IconButton.Tonal font.pointSize: Appearance.font.size.large onClicked: { Recorder.togglePause(); internalChecked = Recorder.paused; } } IconButton { icon: "stop" inactiveColour: Colours.palette.m3error inactiveOnColour: Colours.palette.m3onError font.pointSize: Appearance.font.size.large onClicked: Recorder.stop() } } } } ================================================ FILE: modules/utilities/cards/RecordingList.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.components.containers import qs.services import qs.config import qs.utils import Caelestia.Models import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property var props required property DrawerVisibilities visibilities spacing: 0 WrapperMouseArea { Layout.fillWidth: true cursorShape: Qt.PointingHandCursor onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded RowLayout { spacing: Appearance.spacing.smaller MaterialIcon { Layout.alignment: Qt.AlignVCenter text: "list" font.pointSize: Appearance.font.size.large } StyledText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true text: qsTr("Recordings") font.pointSize: Appearance.font.size.normal } IconButton { icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more" type: IconButton.Text label.animate: true onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded } } } StyledListView { id: list model: FileSystemModel { path: Paths.recsdir nameFilters: ["recording_*.mp4"] sortReverse: true } Layout.fillWidth: true Layout.rightMargin: -Appearance.spacing.small implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3) clip: true StyledScrollBar.vertical: StyledScrollBar { flickable: list } delegate: RowLayout { id: recording required property FileSystemEntry modelData property string baseName anchors.left: list.contentItem.left anchors.right: list.contentItem.right anchors.rightMargin: Appearance.spacing.small spacing: Appearance.spacing.small / 2 Component.onCompleted: baseName = modelData.baseName StyledText { Layout.fillWidth: true Layout.rightMargin: Appearance.spacing.small / 2 text: { const time = recording.baseName; const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/); if (!matches) return time; const date = new Date(...matches.slice(1)); date.setMonth(date.getMonth() - 1); // Woe (months start from 0) return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale())); } color: Colours.palette.m3onSurfaceVariant elide: Text.ElideRight } IconButton { icon: "play_arrow" type: IconButton.Text onClicked: { root.visibilities.utilities = false; root.visibilities.sidebar = false; Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]); } } IconButton { icon: "folder" type: IconButton.Text onClicked: { root.visibilities.utilities = false; root.visibilities.sidebar = false; Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]); } } IconButton { icon: "delete_forever" type: IconButton.Text label.color: Colours.palette.m3error stateLayer.color: Colours.palette.m3error onClicked: root.props.recordingConfirmDelete = recording.modelData.path } } add: Transition { Anim { property: "opacity" from: 0 to: 1 } Anim { property: "scale" from: 0.5 to: 1 } } remove: Transition { Anim { property: "opacity" to: 0 } Anim { property: "scale" to: 0.5 } } displaced: Transition { Anim { properties: "opacity,scale" to: 1 } Anim { property: "y" } } Loader { asynchronous: true anchors.centerIn: parent opacity: list.count === 0 ? 1 : 0 active: opacity > 0 sourceComponent: ColumnLayout { spacing: Appearance.spacing.small MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.extraLarge opacity: root.props.recordingListExpanded ? 1 : 0 scale: root.props.recordingListExpanded ? 1 : 0 Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0 Behavior on opacity { Anim {} } Behavior on scale { Anim {} } Behavior on Layout.preferredHeight { Anim {} } } RowLayout { spacing: Appearance.spacing.smaller MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline opacity: !root.props.recordingListExpanded ? 1 : 0 scale: !root.props.recordingListExpanded ? 1 : 0 Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0 Behavior on opacity { Anim {} } Behavior on scale { Anim {} } Behavior on Layout.preferredWidth { Anim {} } } StyledText { text: qsTr("No recordings found") color: Colours.palette.m3outline } } } Behavior on opacity { Anim {} } } Behavior on implicitHeight { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } ================================================ FILE: modules/utilities/cards/Toggles.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.components.controls import qs.services import qs.config import qs.modules.bar.popouts as BarPopouts import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts StyledRect { id: root required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts readonly property var quickToggles: { const seenIds = new Set(); return Config.utilities.quickToggles.filter(item => { if (!item.enabled) return false; if (seenIds.has(item.id)) { return false; } if (item.id === "vpn") { return Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); } seenIds.add(item.id); return true; }); } readonly property int splitIndex: Math.ceil(quickToggles.length / 2) readonly property bool needExtraRow: quickToggles.length > 6 Layout.fillWidth: true implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: layout anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { text: qsTr("Quick Toggles") font.pointSize: Appearance.font.size.normal } QuickToggleRow { rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles } QuickToggleRow { visible: root.needExtraRow rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] } } component QuickToggleRow: RowLayout { property var rowModel: [] Layout.fillWidth: true spacing: Appearance.spacing.small Repeater { model: parent.rowModel delegate: DelegateChooser { role: "id" DelegateChoice { roleValue: "wifi" delegate: Toggle { icon: "wifi" checked: Nmcli.wifiEnabled onClicked: Nmcli.toggleWifi() } } DelegateChoice { roleValue: "bluetooth" delegate: Toggle { icon: "bluetooth" checked: Bluetooth.defaultAdapter?.enabled ?? false onClicked: { const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = !adapter.enabled; } } } DelegateChoice { roleValue: "mic" delegate: Toggle { icon: "mic" checked: !Audio.sourceMuted onClicked: { const audio = Audio.source?.audio; if (audio) audio.muted = !audio.muted; } } } DelegateChoice { roleValue: "settings" delegate: Toggle { icon: "settings" inactiveOnColour: Colours.palette.m3onSurfaceVariant toggle: false onClicked: { root.visibilities.utilities = false; root.popouts.detach("network"); } } } DelegateChoice { roleValue: "gameMode" delegate: Toggle { icon: "gamepad" checked: GameMode.enabled onClicked: GameMode.enabled = !GameMode.enabled } } DelegateChoice { roleValue: "dnd" delegate: Toggle { icon: "notifications_off" checked: Notifs.dnd onClicked: Notifs.dnd = !Notifs.dnd } } DelegateChoice { roleValue: "vpn" delegate: Toggle { icon: "vpn_key" checked: VPN.connected enabled: !VPN.connecting onClicked: VPN.toggle() } } } } } component Toggle: IconButton { Layout.fillWidth: true Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) toggle: true radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { duration: Appearance.anim.durations.expressiveFastSpatial easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial } } } } ================================================ FILE: modules/utilities/toasts/ToastItem.qml ================================================ import qs.components import qs.components.effects import qs.services import qs.config import Caelestia import QtQuick import QtQuick.Layouts StyledRect { id: root required property Toast modelData anchors.left: parent.left anchors.right: parent.right implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.normal color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3successContainer; if (root.modelData.type === Toast.Warning) return Colours.palette.m3secondary; if (root.modelData.type === Toast.Error) return Colours.palette.m3errorContainer; return Colours.palette.m3surface; } border.width: 1 border.color: { let colour = Colours.palette.m3outlineVariant; if (root.modelData.type === Toast.Success) colour = Colours.palette.m3success; if (root.modelData.type === Toast.Warning) colour = Colours.palette.m3secondaryContainer; if (root.modelData.type === Toast.Error) colour = Colours.palette.m3error; return Qt.alpha(colour, 0.3); } Elevation { anchors.fill: parent radius: parent.radius opacity: parent.opacity z: -1 level: 3 } RowLayout { id: layout anchors.fill: parent anchors.margins: Appearance.padding.smaller anchors.leftMargin: Appearance.padding.normal anchors.rightMargin: Appearance.padding.normal spacing: Appearance.spacing.normal StyledRect { radius: Appearance.rounding.normal color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3success; if (root.modelData.type === Toast.Warning) return Colours.palette.m3secondaryContainer; if (root.modelData.type === Toast.Error) return Colours.palette.m3error; return Colours.palette.m3surfaceContainerHigh; } implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 MaterialIcon { id: icon anchors.centerIn: parent text: root.modelData.icon color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3onSuccess; if (root.modelData.type === Toast.Warning) return Colours.palette.m3onSecondaryContainer; if (root.modelData.type === Toast.Error) return Colours.palette.m3onError; return Colours.palette.m3onSurfaceVariant; } font.pointSize: Math.round(Appearance.font.size.large * 1.2) } } ColumnLayout { Layout.fillWidth: true spacing: 0 StyledText { id: title Layout.fillWidth: true text: root.modelData.title color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3onSuccessContainer; if (root.modelData.type === Toast.Warning) return Colours.palette.m3onSecondary; if (root.modelData.type === Toast.Error) return Colours.palette.m3onErrorContainer; return Colours.palette.m3onSurface; } font.pointSize: Appearance.font.size.normal elide: Text.ElideRight } StyledText { Layout.fillWidth: true textFormat: Text.StyledText text: root.modelData.message color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3onSuccessContainer; if (root.modelData.type === Toast.Warning) return Colours.palette.m3onSecondary; if (root.modelData.type === Toast.Error) return Colours.palette.m3onErrorContainer; return Colours.palette.m3onSurface; } opacity: 0.8 elide: Text.ElideRight } } } Behavior on border.color { CAnim {} } } ================================================ FILE: modules/utilities/toasts/Toasts.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.config import Caelestia import Quickshell import QtQuick Item { id: root readonly property int spacing: Appearance.spacing.small property bool flag implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 implicitHeight: { let h = -spacing; for (let i = 0; i < repeater.count; i++) { const item = repeater.itemAt(i) as ToastWrapper; if (!item.modelData.closed && !item.previewHidden) h += item.implicitHeight + spacing; } return h; } Repeater { id: repeater model: ScriptModel { values: { const toasts = []; let count = 0; for (const toast of Toaster.toasts) { toasts.push(toast); if (!toast.closed) { count++; if (count > Config.utilities.maxToasts) break; } } return toasts; } onValuesChanged: root.flagChanged() } ToastWrapper {} } component ToastWrapper: MouseArea { id: toast required property int index required property Toast modelData readonly property bool previewHidden: { let extraHidden = 0; for (let i = 0; i < index; i++) if (Toaster.toasts[i].closed) extraHidden++; return index >= Config.utilities.maxToasts + extraHidden; } onPreviewHiddenChanged: { if (initAnim.running && previewHidden) initAnim.stop(); } opacity: modelData.closed || previewHidden ? 0 : 1 scale: modelData.closed || previewHidden ? 0.7 : 1 anchors.bottomMargin: { root.flag; // Force update let y = 0; for (let i = 0; i < index; i++) { const item = repeater.itemAt(i) as ToastWrapper; if (item && !item.modelData.closed && !item.previewHidden) y += item.implicitHeight + root.spacing; } return y; } anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom implicitHeight: toastInner.implicitHeight acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton onClicked: modelData.close() Component.onCompleted: modelData.lock(this) Anim { id: initAnim Component.onCompleted: running = !toast.previewHidden target: toast properties: "opacity,scale" from: 0 to: 1 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } ParallelAnimation { running: toast.modelData.closed onStarted: toast.anchors.bottomMargin = toast.anchors.bottomMargin onFinished: toast.modelData.unlock(toast) Anim { target: toast property: "opacity" to: 0 } Anim { target: toast property: "scale" to: 0.7 } } ToastItem { id: toastInner modelData: toast.modelData } Behavior on opacity { Anim {} } Behavior on scale { Anim {} } Behavior on anchors.bottomMargin { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } } ================================================ FILE: modules/windowinfo/Buttons.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell.Widgets import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property var client property bool moveToWsExpanded anchors.fill: parent spacing: Appearance.spacing.small RowLayout { Layout.topMargin: Appearance.padding.large Layout.leftMargin: Appearance.padding.large Layout.rightMargin: Appearance.padding.large spacing: Appearance.spacing.normal StyledText { Layout.fillWidth: true text: qsTr("Move to workspace") elide: Text.ElideRight } StyledRect { color: Colours.palette.m3primary radius: Appearance.rounding.small implicitWidth: moveToWsIcon.implicitWidth + Appearance.padding.small * 2 implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small StateLayer { function onClicked(): void { root.moveToWsExpanded = !root.moveToWsExpanded; } color: Colours.palette.m3onPrimary } MaterialIcon { id: moveToWsIcon anchors.centerIn: parent animate: true text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right" color: Colours.palette.m3onPrimary font.pointSize: Appearance.font.size.large } } } WrapperItem { Layout.fillWidth: true Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0 clip: true topMargin: Appearance.spacing.normal bottomMargin: Appearance.spacing.normal GridLayout { id: wsGrid rowSpacing: Appearance.spacing.smaller columnSpacing: Appearance.spacing.normal columns: 5 Repeater { model: 10 Button { required property int index readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 readonly property bool isCurrent: root.client?.workspace.id === wsId function onClicked(): void { Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); } color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer text: wsId disabled: isCurrent } } } Behavior on Layout.preferredHeight { Anim {} } } RowLayout { Layout.fillWidth: true Layout.leftMargin: Appearance.padding.large Layout.rightMargin: Appearance.padding.large Layout.bottomMargin: Appearance.padding.large spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small Button { function onClicked(): void { Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); } color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") } Loader { asynchronous: true active: root.client?.lastIpcObject.floating Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing Layout.rightMargin: active ? 0 : -parent.spacing sourceComponent: Button { function onClicked(): void { Hypr.dispatch(`pin address:0x${root.client?.address}`); } color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") } } Button { function onClicked(): void { Hypr.dispatch(`killwindow address:0x${root.client?.address}`); } color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Kill") } } component Button: StyledRect { property color onColor: Colours.palette.m3onSurface property alias disabled: stateLayer.disabled property alias text: label.text function onClicked(): void { } radius: Appearance.rounding.small Layout.fillWidth: true implicitHeight: label.implicitHeight + Appearance.padding.small * 2 StateLayer { id: stateLayer function onClicked(): void { parent.onClicked(); } color: parent.onColor } StyledText { id: label anchors.centerIn: parent animate: true color: parent.onColor font.pointSize: Appearance.font.size.normal } } } ================================================ FILE: modules/windowinfo/Details.qml ================================================ import qs.components import qs.services import qs.config import Quickshell.Hyprland import QtQuick import QtQuick.Layouts ColumnLayout { id: root required property HyprlandToplevel client anchors.fill: parent spacing: Appearance.spacing.small Label { Layout.topMargin: Appearance.padding.large * 2 text: root.client?.title ?? qsTr("No active client") wrapMode: Text.WrapAtWordBoundaryOrAnywhere font.pointSize: Appearance.font.size.large font.weight: 500 } Label { text: root.client?.lastIpcObject.class ?? qsTr("No active client") color: Colours.palette.m3tertiary font.pointSize: Appearance.font.size.larger } StyledRect { Layout.fillWidth: true Layout.preferredHeight: 1 Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.topMargin: Appearance.spacing.normal Layout.bottomMargin: Appearance.spacing.large color: Colours.palette.m3secondary } Detail { icon: "location_on" text: qsTr("Address: %1").arg(`0x${root.client?.address}` ?? "unknown") color: Colours.palette.m3primary } Detail { icon: "location_searching" text: qsTr("Position: %1, %2").arg(root.client?.lastIpcObject.at[0] ?? -1).arg(root.client?.lastIpcObject.at[1] ?? -1) } Detail { icon: "resize" text: qsTr("Size: %1 x %2").arg(root.client?.lastIpcObject.size[0] ?? -1).arg(root.client?.lastIpcObject.size[1] ?? -1) color: Colours.palette.m3tertiary } Detail { icon: "workspaces" text: qsTr("Workspace: %1 (%2)").arg(root.client?.workspace.name ?? -1).arg(root.client?.workspace.id ?? -1) color: Colours.palette.m3secondary } Detail { icon: "desktop_windows" text: { const mon = root.client?.monitor; if (mon) return qsTr("Monitor: %1 (%2) at %3, %4").arg(mon.name).arg(mon.id).arg(mon.x).arg(mon.y); return qsTr("Monitor: unknown"); } } Detail { icon: "page_header" text: qsTr("Initial title: %1").arg(root.client?.lastIpcObject.initialTitle ?? "unknown") color: Colours.palette.m3tertiary } Detail { icon: "category" text: qsTr("Initial class: %1").arg(root.client?.lastIpcObject.initialClass ?? "unknown") } Detail { icon: "account_tree" text: qsTr("Process id: %1").arg(root.client?.lastIpcObject.pid ?? -1) color: Colours.palette.m3primary } Detail { icon: "picture_in_picture_center" text: qsTr("Floating: %1").arg(root.client?.lastIpcObject.floating ? "yes" : "no") color: Colours.palette.m3secondary } Detail { icon: "gradient" text: qsTr("Xwayland: %1").arg(root.client?.lastIpcObject.xwayland ? "yes" : "no") } Detail { icon: "keep" text: qsTr("Pinned: %1").arg(root.client?.lastIpcObject.pinned ? "yes" : "no") color: Colours.palette.m3secondary } Detail { icon: "fullscreen" text: { const fs = root.client?.lastIpcObject.fullscreen; if (fs) return qsTr("Fullscreen state: %1").arg(fs == 0 ? "off" : fs == 1 ? "maximised" : "on"); return qsTr("Fullscreen state: unknown"); } color: Colours.palette.m3tertiary } Item { Layout.fillHeight: true } component Detail: RowLayout { id: detail required property string icon required property string text property alias color: icon.color Layout.leftMargin: Appearance.padding.large Layout.rightMargin: Appearance.padding.large Layout.fillWidth: true spacing: Appearance.spacing.smaller MaterialIcon { id: icon Layout.alignment: Qt.AlignVCenter text: detail.icon } StyledText { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter text: detail.text elide: Text.ElideRight font.pointSize: Appearance.font.size.normal } } component Label: StyledText { Layout.leftMargin: Appearance.padding.large Layout.rightMargin: Appearance.padding.large Layout.fillWidth: true elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter animate: true } } ================================================ FILE: modules/windowinfo/Preview.qml ================================================ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick import QtQuick.Layouts Item { id: root required property ShellScreen screen required property HyprlandToplevel client Layout.preferredWidth: preview.implicitWidth + Appearance.padding.large * 2 Layout.fillHeight: true StyledClippingRect { id: preview anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: label.top anchors.topMargin: Appearance.padding.large anchors.bottomMargin: Appearance.spacing.normal implicitWidth: view.implicitWidth color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.small Loader { asynchronous: true anchors.centerIn: parent active: !root.client sourceComponent: ColumnLayout { spacing: 0 MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "web_asset_off" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.extraLarge * 3 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No active client") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.extraLarge font.weight: 500 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Try switching to a window") color: Colours.palette.m3outline font.pointSize: Appearance.font.size.large } } } ScreencopyView { id: view anchors.centerIn: parent captureSource: root.client?.wayland ?? null live: true 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 constraintSize.height: parent.height } } StyledText { id: label anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: Appearance.padding.large animate: true text: { const client = root.client; if (!client) return qsTr("No active client"); const mon = client.monitor; 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]); } } } ================================================ FILE: modules/windowinfo/WindowInfo.qml ================================================ import qs.components import qs.services import qs.config import Quickshell import Quickshell.Hyprland import QtQuick import QtQuick.Layouts Item { id: root required property ShellScreen screen required property HyprlandToplevel client implicitWidth: child.implicitWidth implicitHeight: screen.height * Config.winfo.sizes.heightMult RowLayout { id: child anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal Preview { screen: root.screen client: root.client } ColumnLayout { spacing: Appearance.spacing.normal Layout.preferredWidth: Config.winfo.sizes.detailsWidth Layout.fillHeight: true StyledRect { Layout.fillWidth: true Layout.fillHeight: true color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal Details { client: root.client } } StyledRect { Layout.fillWidth: true Layout.preferredHeight: buttons.implicitHeight color: Colours.tPalette.m3surfaceContainer radius: Appearance.rounding.normal Buttons { id: buttons client: root.client } } } } } ================================================ FILE: nix/default.nix ================================================ { rev, lib, stdenv, makeWrapper, makeFontsConf, fish, ddcutil, brightnessctl, app2unit, networkmanager, lm_sensors, swappy, wl-clipboard, libqalculate, bash, hyprland, material-symbols, rubik, nerd-fonts, qt6, quickshell, aubio, libcava, fftw, pipewire, xkeyboard-config, cmake, ninja, pkg-config, caelestia-cli, debug ? false, withCli ? false, extraRuntimeDeps ? [], }: let version = "1.0.0"; runtimeDeps = [ fish ddcutil brightnessctl app2unit networkmanager lm_sensors swappy wl-clipboard libqalculate bash hyprland ] ++ extraRuntimeDeps ++ lib.optional withCli caelestia-cli; fontconfig = makeFontsConf { fontDirectories = [material-symbols rubik nerd-fonts.caskaydia-cove]; }; cmakeBuildType = if debug then "Debug" else "RelWithDebInfo"; cmakeVersionFlags = [ (lib.cmakeFeature "VERSION" version) (lib.cmakeFeature "GIT_REVISION" rev) (lib.cmakeFeature "DISTRIBUTOR" "nix-flake") ]; extras = stdenv.mkDerivation { inherit cmakeBuildType; name = "caelestia-extras${lib.optionalString debug "-debug"}"; src = lib.fileset.toSource { root = ./..; fileset = lib.fileset.union ./../CMakeLists.txt ./../extras; }; nativeBuildInputs = [cmake ninja]; cmakeFlags = [ (lib.cmakeFeature "ENABLE_MODULES" "extras") (lib.cmakeFeature "INSTALL_LIBDIR" "${placeholder "out"}/lib") ] ++ cmakeVersionFlags; }; plugin = stdenv.mkDerivation { inherit cmakeBuildType; name = "caelestia-qml-plugin${lib.optionalString debug "-debug"}"; src = lib.fileset.toSource { root = ./..; fileset = lib.fileset.union ./../CMakeLists.txt ./../plugin; }; nativeBuildInputs = [cmake ninja pkg-config]; buildInputs = [qt6.qtbase qt6.qtdeclarative libqalculate pipewire aubio libcava fftw]; dontWrapQtApps = true; cmakeFlags = [ (lib.cmakeFeature "ENABLE_MODULES" "plugin") (lib.cmakeFeature "INSTALL_QMLDIR" qt6.qtbase.qtQmlPrefix) ] ++ cmakeVersionFlags; }; in stdenv.mkDerivation { inherit version cmakeBuildType; pname = "caelestia-shell${lib.optionalString debug "-debug"}"; src = ./..; nativeBuildInputs = [cmake ninja makeWrapper qt6.wrapQtAppsHook]; buildInputs = [quickshell extras plugin xkeyboard-config qt6.qtbase]; propagatedBuildInputs = runtimeDeps; cmakeFlags = [ (lib.cmakeFeature "ENABLE_MODULES" "shell") (lib.cmakeFeature "INSTALL_QSCONFDIR" "${placeholder "out"}/share/caelestia-shell") ] ++ cmakeVersionFlags; dontStrip = debug; prePatch = '' substituteInPlace assets/pam.d/fprint \ --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so substituteInPlace shell.qml \ --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' ''; postInstall = '' makeWrapper ${quickshell}/bin/qs $out/bin/caelestia-shell \ --prefix PATH : "${lib.makeBinPath runtimeDeps}" \ --set FONTCONFIG_FILE "${fontconfig}" \ --set CAELESTIA_LIB_DIR ${extras}/lib \ --set CAELESTIA_XKB_RULES_PATH ${xkeyboard-config}/share/xkeyboard-config-2/rules/base.lst \ --add-flags "-p $out/share/caelestia-shell" mkdir -p $out/lib ln -s ${extras}/lib/* $out/lib/ # Ensure wrap_term_launch.sh is executable chmod 755 $out/share/caelestia-shell/assets/wrap_term_launch.sh ''; passthru = { inherit plugin extras; }; meta = { description = "A very segsy desktop shell"; homepage = "https://github.com/caelestia-dots/shell"; license = lib.licenses.gpl3Only; mainProgram = "caelestia-shell"; }; } ================================================ FILE: nix/hm-module.nix ================================================ self: { config, pkgs, lib, ... }: let inherit (pkgs.stdenv.hostPlatform) system; cli-default = self.inputs.caelestia-cli.packages.${system}.default; shell-default = self.packages.${system}.with-cli; cfg = config.programs.caelestia; in { imports = [ (lib.mkRenamedOptionModule ["programs" "caelestia" "environment"] ["programs" "caelestia" "systemd" "environment"]) ]; options = with lib; { programs.caelestia = { enable = mkEnableOption "Enable Caelestia shell"; package = mkOption { type = types.package; default = shell-default; description = "The package of Caelestia shell"; }; systemd = { enable = mkOption { type = types.bool; default = true; description = "Enable the systemd service for Caelestia shell"; }; target = mkOption { type = types.str; description = '' The systemd target that will automatically start the Caelestia shell. ''; default = config.wayland.systemd.target; }; environment = mkOption { type = types.listOf types.str; description = "Extra Environment variables to pass to the Caelestia shell systemd service."; default = []; example = [ "QT_QPA_PLATFORMTHEME=gtk3" ]; }; }; settings = mkOption { type = types.attrsOf types.anything; default = {}; description = "Caelestia shell settings"; }; extraConfig = mkOption { type = types.str; default = ""; description = "Caelestia shell extra configs written to shell.json"; }; cli = { enable = mkEnableOption "Enable Caelestia CLI"; package = mkOption { type = types.package; default = cli-default; description = "The package of Caelestia CLI"; # Doesn't override the shell's CLI, only change from home.packages }; settings = mkOption { type = types.attrsOf types.anything; default = {}; description = "Caelestia CLI settings"; }; extraConfig = mkOption { type = types.str; default = ""; description = "Caelestia CLI extra configs written to cli.json"; }; }; }; }; config = let cli = cfg.cli.package; shell = cfg.package; in lib.mkIf cfg.enable { systemd.user.services.caelestia = lib.mkIf cfg.systemd.enable { Unit = { Description = "Caelestia Shell Service"; After = [cfg.systemd.target]; PartOf = [cfg.systemd.target]; X-Restart-Triggers = lib.mkIf (cfg.settings != {}) [ "${config.xdg.configFile."caelestia/shell.json".source}" ]; }; Service = { Type = "exec"; ExecStart = "${shell}/bin/caelestia-shell"; Restart = "on-failure"; RestartSec = "5s"; TimeoutStopSec = "5s"; Environment = [ "QT_QPA_PLATFORM=wayland" ] ++ cfg.systemd.environment; Slice = "session.slice"; }; Install = { WantedBy = [cfg.systemd.target]; }; }; xdg.configFile = let mkConfig = c: lib.pipe ( if c.extraConfig != "" then c.extraConfig else "{}" ) [ builtins.fromJSON (lib.recursiveUpdate c.settings) builtins.toJSON ]; shouldGenerate = c: c.extraConfig != "" || c.settings != {}; in { "caelestia/shell.json" = lib.mkIf (shouldGenerate cfg) { text = mkConfig cfg; }; "caelestia/cli.json" = lib.mkIf (shouldGenerate cfg.cli) { text = mkConfig cfg.cli; }; }; home.packages = [shell] ++ lib.optional cfg.cli.enable cli; }; } ================================================ FILE: plugin/CMakeLists.txt ================================================ add_subdirectory(src/Caelestia) ================================================ FILE: plugin/src/Caelestia/CMakeLists.txt ================================================ find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) if(NOT Cava_FOUND) pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) endif() set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") qt_standard_project_setup(REQUIRES 6.9) function(qml_module arg_TARGET) cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES") qt_add_qml_module(${arg_TARGET} URI ${arg_URI} VERSION ${VERSION} SOURCES ${arg_SOURCES} ) qt_query_qml_module(${arg_TARGET} URI module_uri VERSION module_version PLUGIN_TARGET module_plugin_target TARGET_PATH module_target_path QMLDIR module_qmldir TYPEINFO module_typeinfo ) message(STATUS "Created QML module ${module_uri}, version ${module_version}") set(module_dir "${INSTALL_QMLDIR}/${module_target_path}") install(TARGETS ${arg_TARGET} LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}") install(TARGETS "${module_plugin_target}" LIBRARY DESTINATION "${module_dir}" RUNTIME DESTINATION "${module_dir}") install(FILES "${module_qmldir}" DESTINATION "${module_dir}") install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES}) endfunction() qml_module(caelestia URI Caelestia SOURCES cutils.hpp cutils.cpp qalculator.hpp qalculator.cpp appdb.hpp appdb.cpp requests.hpp requests.cpp toaster.hpp toaster.cpp imageanalyser.hpp imageanalyser.cpp LIBRARIES Qt::Gui Qt::Quick Qt::Concurrent Qt::Sql PkgConfig::Qalculate ) add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) ================================================ FILE: plugin/src/Caelestia/Internal/CMakeLists.txt ================================================ qml_module(caelestia-internal URI Caelestia.Internal SOURCES arcgauge.hpp arcgauge.cpp cachingimagemanager.hpp cachingimagemanager.cpp circularbuffer.hpp circularbuffer.cpp circularindicatormanager.hpp circularindicatormanager.cpp hyprdevices.hpp hyprdevices.cpp hyprextras.hpp hyprextras.cpp logindmanager.hpp logindmanager.cpp sparklineitem.hpp sparklineitem.cpp LIBRARIES Qt::Gui Qt::Quick Qt::Concurrent Qt::Network Qt::DBus ) ================================================ FILE: plugin/src/Caelestia/Internal/arcgauge.cpp ================================================ #include "arcgauge.hpp" #include #include #include namespace caelestia::internal { ArcGauge::ArcGauge(QQuickItem* parent) : QQuickPaintedItem(parent) { setAntialiasing(true); } void ArcGauge::paint(QPainter* painter) { const qreal w = width(); const qreal h = height(); const qreal side = qMin(w, h); const qreal radius = (side - m_lineWidth - 2.0) / 2.0; const qreal cx = w / 2.0; const qreal cy = h / 2.0; const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0); // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees) const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0); const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0); painter->setRenderHint(QPainter::Antialiasing, true); // Draw track arc QPen trackPen(m_trackColor, m_lineWidth); trackPen.setCapStyle(Qt::RoundCap); painter->setPen(trackPen); painter->setBrush(Qt::NoBrush); painter->drawArc(arcRect, startAngle16, sweepAngle16); // Draw value arc if (m_percentage > 0.0) { const int valueSweep16 = qRound(static_cast(sweepAngle16) * m_percentage); QPen valuePen(m_accentColor, m_lineWidth); valuePen.setCapStyle(Qt::RoundCap); painter->setPen(valuePen); painter->drawArc(arcRect, startAngle16, valueSweep16); } } qreal ArcGauge::percentage() const { return m_percentage; } void ArcGauge::setPercentage(qreal percentage) { if (qFuzzyCompare(m_percentage, percentage)) return; m_percentage = percentage; emit percentageChanged(); update(); } QColor ArcGauge::accentColor() const { return m_accentColor; } void ArcGauge::setAccentColor(const QColor& color) { if (m_accentColor == color) return; m_accentColor = color; emit accentColorChanged(); update(); } QColor ArcGauge::trackColor() const { return m_trackColor; } void ArcGauge::setTrackColor(const QColor& color) { if (m_trackColor == color) return; m_trackColor = color; emit trackColorChanged(); update(); } qreal ArcGauge::startAngle() const { return m_startAngle; } void ArcGauge::setStartAngle(qreal angle) { if (qFuzzyCompare(m_startAngle, angle)) return; m_startAngle = angle; emit startAngleChanged(); update(); } qreal ArcGauge::sweepAngle() const { return m_sweepAngle; } void ArcGauge::setSweepAngle(qreal angle) { if (qFuzzyCompare(m_sweepAngle, angle)) return; m_sweepAngle = angle; emit sweepAngleChanged(); update(); } qreal ArcGauge::lineWidth() const { return m_lineWidth; } void ArcGauge::setLineWidth(qreal width) { if (qFuzzyCompare(m_lineWidth, width)) return; m_lineWidth = width; emit lineWidthChanged(); update(); } } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/arcgauge.hpp ================================================ #pragma once #include #include #include #include namespace caelestia::internal { class ArcGauge : public QQuickPaintedItem { Q_OBJECT QML_ELEMENT Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged) Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged) Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged) Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged) Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) public: explicit ArcGauge(QQuickItem* parent = nullptr); void paint(QPainter* painter) override; [[nodiscard]] qreal percentage() const; void setPercentage(qreal percentage); [[nodiscard]] QColor accentColor() const; void setAccentColor(const QColor& color); [[nodiscard]] QColor trackColor() const; void setTrackColor(const QColor& color); [[nodiscard]] qreal startAngle() const; void setStartAngle(qreal angle); [[nodiscard]] qreal sweepAngle() const; void setSweepAngle(qreal angle); [[nodiscard]] qreal lineWidth() const; void setLineWidth(qreal width); signals: void percentageChanged(); void accentColorChanged(); void trackColorChanged(); void startAngleChanged(); void sweepAngleChanged(); void lineWidthChanged(); private: qreal m_percentage = 0.0; QColor m_accentColor; QColor m_trackColor; qreal m_startAngle = 0.75 * M_PI; qreal m_sweepAngle = 1.5 * M_PI; qreal m_lineWidth = 10.0; }; } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/cachingimagemanager.cpp ================================================ #include "cachingimagemanager.hpp" #include #include #include #include #include #include #include #include namespace caelestia::internal { qreal CachingImageManager::effectiveScale() const { if (m_item && m_item->window()) { return m_item->window()->devicePixelRatio(); } return 1.0; } QSize CachingImageManager::effectiveSize() const { if (!m_item) { return QSize(); } const qreal scale = effectiveScale(); const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); m_item->setProperty("sourceSize", size); return size; } QQuickItem* CachingImageManager::item() const { return m_item; } void CachingImageManager::setItem(QQuickItem* item) { if (m_item == item) { return; } if (m_widthConn) { disconnect(m_widthConn); } if (m_heightConn) { disconnect(m_heightConn); } m_item = item; emit itemChanged(); if (item) { m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { updateSource(); }); m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { updateSource(); }); updateSource(); } } QUrl CachingImageManager::cacheDir() const { return m_cacheDir; } void CachingImageManager::setCacheDir(const QUrl& cacheDir) { if (m_cacheDir == cacheDir) { return; } m_cacheDir = cacheDir; if (!m_cacheDir.path().endsWith("/")) { m_cacheDir.setPath(m_cacheDir.path() + "/"); } emit cacheDirChanged(); } QString CachingImageManager::path() const { return m_path; } void CachingImageManager::setPath(const QString& path) { if (m_path == path) { return; } m_path = path; emit pathChanged(); if (!path.isEmpty()) { updateSource(path); } } void CachingImageManager::updateSource() { updateSource(m_path); } void CachingImageManager::updateSource(const QString& path) { if (path.isEmpty() || path == m_shaPath) { // Path is empty or already calculating sha for path return; } m_shaPath = path; const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); const auto watcher = new QFutureWatcher(this); connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { if (m_path != path) { // Object is destroyed or path has changed, ignore watcher->deleteLater(); return; } const QSize size = effectiveSize(); if (!m_item || !size.width() || !size.height()) { watcher->deleteLater(); return; } const QString fillMode = m_item->property("fillMode").toString(); // clang-format off const QString filename = QString("%1@%2x%3-%4.png") .arg(watcher->result()).arg(size.width()).arg(size.height()) .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); // clang-format on const QUrl cache = m_cacheDir.resolved(QUrl(filename)); if (m_cachePath == cache) { watcher->deleteLater(); return; } m_cachePath = cache; emit cachePathChanged(); if (!cache.isLocalFile()) { qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; watcher->deleteLater(); return; } const QImageReader reader(cache.toLocalFile()); if (reader.canRead()) { m_item->setProperty("source", cache); } else { m_item->setProperty("source", QUrl::fromLocalFile(path)); createCache(path, cache.toLocalFile(), fillMode, size); } // Clear current running sha if same if (m_shaPath == path) { m_shaPath = QString(); } watcher->deleteLater(); }); watcher->setFuture(future); } QUrl CachingImageManager::cachePath() const { return m_cachePath; } void CachingImageManager::createCache( const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { QThreadPool::globalInstance()->start([path, cache, fillMode, size] { QImage image(path); if (image.isNull()) { qWarning() << "CachingImageManager::createCache: failed to read" << path; return; } image.convertTo(QImage::Format_ARGB32); if (fillMode == "PreserveAspectCrop") { image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); } else if (fillMode == "PreserveAspectFit") { image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); } else { image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") { QImage canvas(size, QImage::Format_ARGB32); canvas.fill(Qt::transparent); QPainter painter(&canvas); painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); painter.end(); image = canvas; } const QString parent = QFileInfo(cache).absolutePath(); if (!QDir().mkpath(parent) || !image.save(cache)) { qWarning() << "CachingImageManager::createCache: failed to save to" << cache; } }); } QString CachingImageManager::sha256sum(const QString& path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "CachingImageManager::sha256sum: failed to open" << path; return ""; } QCryptographicHash hash(QCryptographicHash::Sha256); hash.addData(&file); file.close(); return hash.result().toHex(); } } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/cachingimagemanager.hpp ================================================ #pragma once #include #include #include namespace caelestia::internal { class CachingImageManager : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) public: explicit CachingImageManager(QObject* parent = nullptr) : QObject(parent) , m_item(nullptr) {} [[nodiscard]] QQuickItem* item() const; void setItem(QQuickItem* item); [[nodiscard]] QUrl cacheDir() const; void setCacheDir(const QUrl& cacheDir); [[nodiscard]] QString path() const; void setPath(const QString& path); [[nodiscard]] QUrl cachePath() const; Q_INVOKABLE void updateSource(); Q_INVOKABLE void updateSource(const QString& path); signals: void itemChanged(); void cacheDirChanged(); void pathChanged(); void cachePathChanged(); void usingCacheChanged(); private: QString m_shaPath; QQuickItem* m_item; QUrl m_cacheDir; QString m_path; QUrl m_cachePath; QMetaObject::Connection m_widthConn; QMetaObject::Connection m_heightConn; [[nodiscard]] qreal effectiveScale() const; [[nodiscard]] QSize effectiveSize() const; void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; [[nodiscard]] static QString sha256sum(const QString& path); }; } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/circularbuffer.cpp ================================================ #include "circularbuffer.hpp" #include namespace caelestia::internal { CircularBuffer::CircularBuffer(QObject* parent) : QObject(parent) {} int CircularBuffer::capacity() const { return m_capacity; } void CircularBuffer::setCapacity(int capacity) { if (capacity < 0) capacity = 0; if (m_capacity == capacity) return; const auto old = values(); m_capacity = capacity; m_data.resize(capacity); m_data.fill(0.0); m_head = 0; m_count = 0; // Re-push old values, keeping the most recent ones const auto start = old.size() > capacity ? old.size() - capacity : 0; for (auto i = start; i < old.size(); ++i) { m_data[m_head] = old[i]; m_head = (m_head + 1) % m_capacity; m_count++; } emit capacityChanged(); emit countChanged(); emit valuesChanged(); } int CircularBuffer::count() const { return m_count; } QList CircularBuffer::values() const { QList result; result.reserve(m_count); for (int i = 0; i < m_count; ++i) result.append(at(i)); return result; } qreal CircularBuffer::maximum() const { if (m_count == 0) return 0.0; qreal maxVal = at(0); for (int i = 1; i < m_count; ++i) maxVal = std::max(maxVal, at(i)); return maxVal; } void CircularBuffer::push(qreal value) { if (m_capacity <= 0) return; m_data[m_head] = value; m_head = (m_head + 1) % m_capacity; if (m_count < m_capacity) { m_count++; emit countChanged(); } emit valuesChanged(); } void CircularBuffer::clear() { if (m_count == 0) return; m_head = 0; m_count = 0; emit countChanged(); emit valuesChanged(); } qreal CircularBuffer::at(int index) const { if (index < 0 || index >= m_count) return 0.0; const int actualIndex = (m_head - m_count + index + m_capacity) % m_capacity; return m_data[actualIndex]; } } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/circularbuffer.hpp ================================================ #pragma once #include #include #include namespace caelestia::internal { class CircularBuffer : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(int capacity READ capacity WRITE setCapacity NOTIFY capacityChanged) Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(QList values READ values NOTIFY valuesChanged) Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged) public: explicit CircularBuffer(QObject* parent = nullptr); [[nodiscard]] int capacity() const; void setCapacity(int capacity); [[nodiscard]] int count() const; [[nodiscard]] QList values() const; [[nodiscard]] qreal maximum() const; Q_INVOKABLE void push(qreal value); Q_INVOKABLE void clear(); Q_INVOKABLE [[nodiscard]] qreal at(int index) const; signals: void capacityChanged(); void countChanged(); void valuesChanged(); private: QVector m_data; int m_head = 0; int m_count = 0; int m_capacity = 0; }; } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/circularindicatormanager.cpp ================================================ #include "circularindicatormanager.hpp" #include #include namespace { namespace advance { constexpr qint32 TOTAL_CYCLES = 4; constexpr qint32 TOTAL_DURATION_IN_MS = 5400; constexpr qint32 DURATION_TO_EXPAND_IN_MS = 667; constexpr qint32 DURATION_TO_COLLAPSE_IN_MS = 667; constexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 333; constexpr qint32 TAIL_DEGREES_OFFSET = -20; constexpr qint32 EXTRA_DEGREES_PER_CYCLE = 250; constexpr qint32 CONSTANT_ROTATION_DEGREES = 1520; constexpr std::array DELAY_TO_EXPAND_IN_MS = { 0, 1350, 2700, 4050 }; constexpr std::array DELAY_TO_COLLAPSE_IN_MS = { 667, 2017, 3367, 4717 }; } // namespace advance namespace retreat { constexpr qint32 TOTAL_DURATION_IN_MS = 6000; constexpr qint32 DURATION_SPIN_IN_MS = 500; constexpr qint32 DURATION_GROW_ACTIVE_IN_MS = 3000; constexpr qint32 DURATION_SHRINK_ACTIVE_IN_MS = 3000; constexpr std::array DELAY_SPINS_IN_MS = { 0, 1500, 3000, 4500 }; constexpr qint32 DELAY_GROW_ACTIVE_IN_MS = 0; constexpr qint32 DELAY_SHRINK_ACTIVE_IN_MS = 3000; constexpr qint32 DURATION_TO_COMPLETE_END_IN_MS = 500; // Constants for animation values. // The total degrees that a constant rotation goes by. constexpr qint32 CONSTANT_ROTATION_DEGREES = 1080; // Despite of the constant rotation, there are also 5 extra rotations the entire animation. The // total degrees that each extra rotation goes by. constexpr qint32 SPIN_ROTATION_DEGREES = 90; constexpr std::array END_FRACTION_RANGE = { 0.10, 0.87 }; } // namespace retreat inline qreal getFractionInRange(qreal playtime, qreal start, qreal duration) { const auto fraction = (playtime - start) / duration; return std::clamp(fraction, 0.0, 1.0); } } // namespace namespace caelestia::internal { CircularIndicatorManager::CircularIndicatorManager(QObject* parent) : QObject(parent) , m_type(IndeterminateAnimationType::Advance) , m_curve(QEasingCurve(QEasingCurve::BezierSpline)) , m_progress(0) , m_startFraction(0) , m_endFraction(0) , m_rotation(0) , m_completeEndProgress(0) { // Fast out slow in m_curve.addCubicBezierSegment({ 0.4, 0.0 }, { 0.2, 1.0 }, { 1.0, 1.0 }); } qreal CircularIndicatorManager::startFraction() const { return m_startFraction; } qreal CircularIndicatorManager::endFraction() const { return m_endFraction; } qreal CircularIndicatorManager::rotation() const { return m_rotation; } qreal CircularIndicatorManager::progress() const { return m_progress; } void CircularIndicatorManager::setProgress(qreal progress) { update(progress); } qreal CircularIndicatorManager::duration() const { if (m_type == IndeterminateAnimationType::Advance) { return advance::TOTAL_DURATION_IN_MS; } else { return retreat::TOTAL_DURATION_IN_MS; } } qreal CircularIndicatorManager::completeEndDuration() const { if (m_type == IndeterminateAnimationType::Advance) { return advance::DURATION_TO_COMPLETE_END_IN_MS; } else { return retreat::DURATION_TO_COMPLETE_END_IN_MS; } } CircularIndicatorManager::IndeterminateAnimationType CircularIndicatorManager::indeterminateAnimationType() const { return m_type; } void CircularIndicatorManager::setIndeterminateAnimationType(IndeterminateAnimationType t) { if (m_type != t) { m_type = t; emit indeterminateAnimationTypeChanged(); } } qreal CircularIndicatorManager::completeEndProgress() const { return m_completeEndProgress; } void CircularIndicatorManager::setCompleteEndProgress(qreal progress) { if (qFuzzyCompare(m_completeEndProgress + 1.0, progress + 1.0)) { return; } m_completeEndProgress = progress; emit completeEndProgressChanged(); update(m_progress); } void CircularIndicatorManager::update(qreal progress) { if (qFuzzyCompare(m_progress + 1.0, progress + 1.0)) { return; } if (m_type == IndeterminateAnimationType::Advance) { updateAdvance(progress); } else { updateRetreat(progress); } m_progress = progress; emit progressChanged(); } void CircularIndicatorManager::updateRetreat(qreal progress) { using namespace retreat; const auto playtime = progress * TOTAL_DURATION_IN_MS; // Constant rotation. const qreal constantRotation = CONSTANT_ROTATION_DEGREES * progress; // Extra rotation for the faster spinning. qreal spinRotation = 0; for (const int spinDelay : DELAY_SPINS_IN_MS) { spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) * SPIN_ROTATION_DEGREES; } const auto oldRotation = m_rotation; m_rotation = constantRotation + spinRotation; if (!qFuzzyCompare(m_rotation + 1.0, oldRotation + 1.0)) emit rotationChanged(); // Grow active indicator. qreal fraction = m_curve.valueForProgress(getFractionInRange(playtime, DELAY_GROW_ACTIVE_IN_MS, DURATION_GROW_ACTIVE_IN_MS)); fraction -= m_curve.valueForProgress(getFractionInRange(playtime, DELAY_SHRINK_ACTIVE_IN_MS, DURATION_SHRINK_ACTIVE_IN_MS)); if (!qFuzzyIsNull(m_startFraction)) { m_startFraction = 0.0; emit startFractionChanged(); } const auto oldEndFrac = m_endFraction; m_endFraction = std::lerp(END_FRACTION_RANGE[0], END_FRACTION_RANGE[1], fraction); // Completing animation. if (m_completeEndProgress > 0) { m_endFraction *= 1 - m_completeEndProgress; } if (!qFuzzyCompare(m_endFraction + 1.0, oldEndFrac + 1.0)) { emit endFractionChanged(); } } void CircularIndicatorManager::updateAdvance(qreal progress) { using namespace advance; const auto playtime = progress * TOTAL_DURATION_IN_MS; const auto oldStart = m_startFraction; const auto oldEnd = m_endFraction; // Adds constant rotation to segment positions. m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET; m_endFraction = CONSTANT_ROTATION_DEGREES * progress; // Adds cycle specific rotation to segment positions. for (size_t cycleIndex = 0; cycleIndex < TOTAL_CYCLES; ++cycleIndex) { // While expanding. qreal fraction = getFractionInRange(playtime, DELAY_TO_EXPAND_IN_MS[cycleIndex], DURATION_TO_EXPAND_IN_MS); m_endFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE; // While collapsing. fraction = getFractionInRange(playtime, DELAY_TO_COLLAPSE_IN_MS[cycleIndex], DURATION_TO_COLLAPSE_IN_MS); m_startFraction += m_curve.valueForProgress(fraction) * EXTRA_DEGREES_PER_CYCLE; } // Closes the gap between head and tail for complete end. m_startFraction += (m_endFraction - m_startFraction) * m_completeEndProgress; m_startFraction /= 360.0; m_endFraction /= 360.0; if (!qFuzzyCompare(m_startFraction + 1.0, oldStart + 1.0)) emit startFractionChanged(); if (!qFuzzyCompare(m_endFraction + 1.0, oldEnd + 1.0)) emit endFractionChanged(); } } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/circularindicatormanager.hpp ================================================ #pragma once #include #include #include namespace caelestia::internal { class CircularIndicatorManager : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(qreal startFraction READ startFraction NOTIFY startFractionChanged) Q_PROPERTY(qreal endFraction READ endFraction NOTIFY endFractionChanged) Q_PROPERTY(qreal rotation READ rotation NOTIFY rotationChanged) Q_PROPERTY(qreal progress READ progress WRITE setProgress NOTIFY progressChanged) Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE setCompleteEndProgress NOTIFY completeEndProgressChanged) Q_PROPERTY(qreal duration READ duration NOTIFY indeterminateAnimationTypeChanged) Q_PROPERTY(qreal completeEndDuration READ completeEndDuration NOTIFY indeterminateAnimationTypeChanged) Q_PROPERTY(IndeterminateAnimationType indeterminateAnimationType READ indeterminateAnimationType WRITE setIndeterminateAnimationType NOTIFY indeterminateAnimationTypeChanged) public: explicit CircularIndicatorManager(QObject* parent = nullptr); enum IndeterminateAnimationType { Advance = 0, Retreat }; Q_ENUM(IndeterminateAnimationType) [[nodiscard]] qreal startFraction() const; [[nodiscard]] qreal endFraction() const; [[nodiscard]] qreal rotation() const; [[nodiscard]] qreal progress() const; void setProgress(qreal progress); [[nodiscard]] qreal completeEndProgress() const; void setCompleteEndProgress(qreal progress); [[nodiscard]] qreal duration() const; [[nodiscard]] qreal completeEndDuration() const; [[nodiscard]] IndeterminateAnimationType indeterminateAnimationType() const; void setIndeterminateAnimationType(IndeterminateAnimationType t); signals: void startFractionChanged(); void endFractionChanged(); void rotationChanged(); void progressChanged(); void completeEndProgressChanged(); void indeterminateAnimationTypeChanged(); private: IndeterminateAnimationType m_type; QEasingCurve m_curve; qreal m_progress; qreal m_startFraction; qreal m_endFraction; qreal m_rotation; qreal m_completeEndProgress; void update(qreal progress); void updateAdvance(qreal progress); void updateRetreat(qreal progress); }; } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/hyprdevices.cpp ================================================ #include "hyprdevices.hpp" #include namespace caelestia::internal::hypr { HyprKeyboard::HyprKeyboard(QJsonObject ipcObject, QObject* parent) : QObject(parent) , m_lastIpcObject(ipcObject) {} QVariantHash HyprKeyboard::lastIpcObject() const { return m_lastIpcObject.toVariantHash(); } QString HyprKeyboard::address() const { return m_lastIpcObject.value("address").toString(); } QString HyprKeyboard::name() const { return m_lastIpcObject.value("name").toString(); } QString HyprKeyboard::layout() const { return m_lastIpcObject.value("layout").toString(); } QString HyprKeyboard::activeKeymap() const { return m_lastIpcObject.value("active_keymap").toString(); } bool HyprKeyboard::capsLock() const { return m_lastIpcObject.value("capsLock").toBool(); } bool HyprKeyboard::numLock() const { return m_lastIpcObject.value("numLock").toBool(); } bool HyprKeyboard::main() const { return m_lastIpcObject.value("main").toBool(); } bool HyprKeyboard::updateLastIpcObject(QJsonObject object) { if (m_lastIpcObject == object) { return false; } const auto last = m_lastIpcObject; m_lastIpcObject = object; emit lastIpcObjectChanged(); bool dirty = false; if (last.value("address") != object.value("address")) { dirty = true; emit addressChanged(); } if (last.value("name") != object.value("name")) { dirty = true; emit nameChanged(); } if (last.value("layout") != object.value("layout")) { dirty = true; emit layoutChanged(); } if (last.value("active_keymap") != object.value("active_keymap")) { dirty = true; emit activeKeymapChanged(); } if (last.value("capsLock") != object.value("capsLock")) { dirty = true; emit capsLockChanged(); } if (last.value("numLock") != object.value("numLock")) { dirty = true; emit numLockChanged(); } if (last.value("main") != object.value("main")) { dirty = true; emit mainChanged(); } return dirty; } HyprDevices::HyprDevices(QObject* parent) : QObject(parent) {} QQmlListProperty HyprDevices::keyboards() { return QQmlListProperty(this, &m_keyboards); } bool HyprDevices::updateLastIpcObject(QJsonObject object) { const auto val = object.value("keyboards").toArray(); bool dirty = false; for (auto it = m_keyboards.begin(); it != m_keyboards.end();) { auto* const keyboard = *it; const auto inNewValues = std::any_of(val.begin(), val.end(), [keyboard](const QJsonValue& o) { return o.toObject().value("address").toString() == keyboard->address(); }); if (!inNewValues) { dirty = true; it = m_keyboards.erase(it); keyboard->deleteLater(); } else { ++it; } } for (const auto& o : val) { const auto obj = o.toObject(); const auto addr = obj.value("address").toString(); auto it = std::find_if(m_keyboards.begin(), m_keyboards.end(), [addr](const HyprKeyboard* kb) { return kb->address() == addr; }); if (it != m_keyboards.end()) { dirty |= (*it)->updateLastIpcObject(obj); } else { dirty = true; m_keyboards << new HyprKeyboard(obj, this); } } if (dirty) { emit keyboardsChanged(); } return dirty; } } // namespace caelestia::internal::hypr ================================================ FILE: plugin/src/Caelestia/Internal/hyprdevices.hpp ================================================ #pragma once #include #include #include #include namespace caelestia::internal::hypr { class HyprKeyboard : public QObject { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("HyprKeyboard instances can only be retrieved from a HyprDevices") Q_PROPERTY(QVariantHash lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged) Q_PROPERTY(QString address READ address NOTIFY addressChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString layout READ layout NOTIFY layoutChanged) Q_PROPERTY(QString activeKeymap READ activeKeymap NOTIFY activeKeymapChanged) Q_PROPERTY(bool capsLock READ capsLock NOTIFY capsLockChanged) Q_PROPERTY(bool numLock READ numLock NOTIFY numLockChanged) Q_PROPERTY(bool main READ main NOTIFY mainChanged) public: explicit HyprKeyboard(QJsonObject ipcObject, QObject* parent = nullptr); [[nodiscard]] QVariantHash lastIpcObject() const; [[nodiscard]] QString address() const; [[nodiscard]] QString name() const; [[nodiscard]] QString layout() const; [[nodiscard]] QString activeKeymap() const; [[nodiscard]] bool capsLock() const; [[nodiscard]] bool numLock() const; [[nodiscard]] bool main() const; bool updateLastIpcObject(QJsonObject object); signals: void lastIpcObjectChanged(); void addressChanged(); void nameChanged(); void layoutChanged(); void activeKeymapChanged(); void capsLockChanged(); void numLockChanged(); void mainChanged(); private: QJsonObject m_lastIpcObject; }; class HyprDevices : public QObject { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("HyprDevices instances can only be retrieved from a HyprExtras") Q_PROPERTY( QQmlListProperty keyboards READ keyboards NOTIFY keyboardsChanged) public: explicit HyprDevices(QObject* parent = nullptr); [[nodiscard]] QQmlListProperty keyboards(); bool updateLastIpcObject(QJsonObject object); signals: void keyboardsChanged(); private: QList m_keyboards; }; } // namespace caelestia::internal::hypr ================================================ FILE: plugin/src/Caelestia/Internal/hyprextras.cpp ================================================ #include "hyprextras.hpp" #include #include #include #include namespace caelestia::internal::hypr { HyprExtras::HyprExtras(QObject* parent) : QObject(parent) , m_requestSocket("") , m_eventSocket("") , m_socket(nullptr) , m_socketValid(false) , m_devices(new HyprDevices(this)) { const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); if (his.isEmpty()) { qWarning() << "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; return; } auto hyprDir = QString("%1/hypr/%2").arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his); if (!QDir(hyprDir).exists()) { hyprDir = "/tmp/hypr/" + his; if (!QDir(hyprDir).exists()) { qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " "Hyprland socket."; return; } } m_requestSocket = hyprDir + "/.socket.sock"; m_eventSocket = hyprDir + "/.socket2.sock"; refreshOptions(); refreshDevices(); m_socket = new QLocalSocket(this); QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError); QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged); QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent); m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly); } QVariantHash HyprExtras::options() const { return m_options; } HyprDevices* HyprExtras::devices() const { return m_devices; } void HyprExtras::message(const QString& message) { if (message.isEmpty()) { return; } makeRequest(message, [](bool success, const QByteArray& res) { if (!success) { qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); } }); } void HyprExtras::batchMessage(const QStringList& messages) { if (messages.isEmpty()) { return; } makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { if (!success) { qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); } }); } void HyprExtras::applyOptions(const QVariantHash& options) { if (options.isEmpty()) { return; } QString request; request.reserve(12 + options.size() * 40); request += QLatin1String("[[BATCH]]"); for (auto it = options.constBegin(); it != options.constEnd(); ++it) { request += QLatin1String("keyword ") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';'); } makeRequest(request, [this](bool success, const QByteArray& res) { if (success) { refreshOptions(); } else { qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); } }); } void HyprExtras::refreshOptions() { if (!m_optionsRefresh.isNull()) { m_optionsRefresh->close(); } m_optionsRefresh = makeRequestJson("descriptions", [this](bool success, const QJsonDocument& response) { m_optionsRefresh.reset(); if (!success) { return; } const auto options = response.array(); bool dirty = false; for (const auto& o : std::as_const(options)) { const auto obj = o.toObject(); const auto key = obj.value("value").toString(); const auto value = obj.value("data").toObject().value("current").toVariant(); if (m_options.value(key) != value) { dirty = true; m_options.insert(key, value); } } if (dirty) { emit optionsChanged(); } }); } void HyprExtras::refreshDevices() { if (!m_devicesRefresh.isNull()) { m_devicesRefresh->close(); } m_devicesRefresh = makeRequestJson("devices", [this](bool success, const QJsonDocument& response) { m_devicesRefresh.reset(); if (success) { m_devices->updateLastIpcObject(response.object()); } }); } void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { if (!m_socketValid) { qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; } else { qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; } } void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::UnconnectedState && m_socketValid) { qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; } m_socketValid = state == QLocalSocket::ConnectedState; } void HyprExtras::readEvent() { while (true) { auto rawEvent = m_socket->readLine(); if (rawEvent.isEmpty()) { break; } rawEvent.truncate(rawEvent.length() - 1); // Remove trailing \n const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(">>")); handleEvent(QString::fromUtf8(event)); } } void HyprExtras::handleEvent(const QString& event) { if (event == "configreloaded") { refreshOptions(); } else if (event == "activelayout") { refreshDevices(); } } HyprExtras::SocketPtr HyprExtras::makeRequestJson( const QString& request, const std::function& callback) { return makeRequest("j/" + request, [callback](bool success, const QByteArray& response) { callback(success, QJsonDocument::fromJson(response)); }); } HyprExtras::SocketPtr HyprExtras::makeRequest( const QString& request, const std::function& callback) { if (m_requestSocket.isEmpty()) { return SocketPtr(); } auto socket = SocketPtr::create(this); QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() { QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() { const auto response = socket->readAll(); callback(true, std::move(response)); socket->close(); }); socket->write(request.toUtf8()); socket->flush(); }); QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; callback(false, {}); socket->close(); }); socket->connectToServer(m_requestSocket); return socket; } } // namespace caelestia::internal::hypr ================================================ FILE: plugin/src/Caelestia/Internal/hyprextras.hpp ================================================ #pragma once #include "hyprdevices.hpp" #include #include #include namespace caelestia::internal::hypr { class HyprExtras : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) Q_PROPERTY(caelestia::internal::hypr::HyprDevices* devices READ devices CONSTANT) public: explicit HyprExtras(QObject* parent = nullptr); [[nodiscard]] QVariantHash options() const; [[nodiscard]] HyprDevices* devices() const; Q_INVOKABLE void message(const QString& message); Q_INVOKABLE void batchMessage(const QStringList& messages); Q_INVOKABLE void applyOptions(const QVariantHash& options); Q_INVOKABLE void refreshOptions(); Q_INVOKABLE void refreshDevices(); signals: void optionsChanged(); private: using SocketPtr = QSharedPointer; QString m_requestSocket; QString m_eventSocket; QLocalSocket* m_socket; bool m_socketValid; QVariantHash m_options; HyprDevices* const m_devices; SocketPtr m_optionsRefresh; SocketPtr m_devicesRefresh; void socketError(QLocalSocket::LocalSocketError error) const; void socketStateChanged(QLocalSocket::LocalSocketState state); void readEvent(); void handleEvent(const QString& event); SocketPtr makeRequestJson(const QString& request, const std::function& callback); SocketPtr makeRequest(const QString& request, const std::function& callback); }; } // namespace caelestia::internal::hypr ================================================ FILE: plugin/src/Caelestia/Internal/logindmanager.cpp ================================================ #include "logindmanager.hpp" #include #include #include #include namespace caelestia::internal { LogindManager::LogindManager(QObject* parent) : QObject(parent) { auto bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { qWarning() << "LogindManager::LogindManager: failed to connect to system bus:" << bus.lastError().message(); return; } bool ok = bus.connect("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "PrepareForSleep", this, SLOT(handlePrepareForSleep(bool))); if (!ok) { qWarning() << "LogindManager::LogindManager: failed to connect to PrepareForSleep signal:" << bus.lastError().message(); } QDBusInterface login1("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", bus); const QDBusReply reply = login1.call("GetSession", "auto"); if (!reply.isValid()) { qWarning() << "LogindManager::LogindManager: failed to get session path"; return; } const auto sessionPath = reply.value().path(); ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Lock", this, SLOT(handleLockRequested())); if (!ok) { qWarning() << "LogindManager::LogindManager: failed to connect to Lock signal:" << bus.lastError().message(); } ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Unlock", this, SLOT(handleUnlockRequested())); if (!ok) { qWarning() << "LogindManager::LogindManager: failed to connect to Unlock signal:" << bus.lastError().message(); } } void LogindManager::handlePrepareForSleep(bool sleep) { if (sleep) { emit aboutToSleep(); } else { emit resumed(); } } void LogindManager::handleLockRequested() { emit lockRequested(); } void LogindManager::handleUnlockRequested() { emit unlockRequested(); } } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/logindmanager.hpp ================================================ #pragma once #include #include namespace caelestia::internal { class LogindManager : public QObject { Q_OBJECT QML_ELEMENT public: explicit LogindManager(QObject* parent = nullptr); signals: void aboutToSleep(); void resumed(); void lockRequested(); void unlockRequested(); private slots: void handlePrepareForSleep(bool sleep); void handleLockRequested(); void handleUnlockRequested(); }; } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/sparklineitem.cpp ================================================ #include "sparklineitem.hpp" #include #include #include namespace caelestia::internal { SparklineItem::SparklineItem(QQuickItem* parent) : QQuickPaintedItem(parent) { setAntialiasing(true); } void SparklineItem::paint(QPainter* painter) { const bool has1 = m_line1 && m_line1->count() >= 2; const bool has2 = m_line2 && m_line2->count() >= 2; if (!has1 && !has2) return; painter->setRenderHint(QPainter::Antialiasing, true); // Draw line1 first (behind), then line2 (in front) if (has1) drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha); if (has2) drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha); } void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { if (m_historyLength < 2) return; const qreal w = width(); const qreal h = height(); const int len = buffer->count(); const qreal stepX = w / static_cast(m_historyLength - 1); const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX; // Build line path QPainterPath linePath; linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h); for (int i = 1; i < len; ++i) { const qreal x = startX + i * stepX; const qreal y = h - (buffer->at(i) / m_maxValue) * h; linePath.lineTo(x, y); } // Stroke the line QPen pen(color, m_lineWidth); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); painter->setPen(pen); painter->setBrush(Qt::NoBrush); painter->drawPath(linePath); // Fill under the line QPainterPath fillPath = linePath; fillPath.lineTo(startX + (len - 1) * stepX, h); fillPath.lineTo(startX, h); fillPath.closeSubpath(); QColor fillColor = color; fillColor.setAlphaF(static_cast(fillAlpha)); painter->setPen(Qt::NoPen); painter->setBrush(fillColor); painter->drawPath(fillPath); } void SparklineItem::connectBuffer(CircularBuffer* buffer) { if (!buffer) return; connect(buffer, &CircularBuffer::valuesChanged, this, [this]() { update(); }); connect(buffer, &QObject::destroyed, this, [this, buffer]() { if (m_line1 == buffer) { m_line1 = nullptr; emit line1Changed(); } if (m_line2 == buffer) { m_line2 = nullptr; emit line2Changed(); } update(); }); } CircularBuffer* SparklineItem::line1() const { return m_line1; } void SparklineItem::setLine1(CircularBuffer* buffer) { if (m_line1 == buffer) return; if (m_line1) disconnect(m_line1, nullptr, this, nullptr); m_line1 = buffer; connectBuffer(buffer); emit line1Changed(); update(); } CircularBuffer* SparklineItem::line2() const { return m_line2; } void SparklineItem::setLine2(CircularBuffer* buffer) { if (m_line2 == buffer) return; if (m_line2) disconnect(m_line2, nullptr, this, nullptr); m_line2 = buffer; connectBuffer(buffer); emit line2Changed(); update(); } QColor SparklineItem::line1Color() const { return m_line1Color; } void SparklineItem::setLine1Color(const QColor& color) { if (m_line1Color == color) return; m_line1Color = color; emit line1ColorChanged(); update(); } QColor SparklineItem::line2Color() const { return m_line2Color; } void SparklineItem::setLine2Color(const QColor& color) { if (m_line2Color == color) return; m_line2Color = color; emit line2ColorChanged(); update(); } qreal SparklineItem::line1FillAlpha() const { return m_line1FillAlpha; } void SparklineItem::setLine1FillAlpha(qreal alpha) { if (qFuzzyCompare(m_line1FillAlpha, alpha)) return; m_line1FillAlpha = alpha; emit line1FillAlphaChanged(); update(); } qreal SparklineItem::line2FillAlpha() const { return m_line2FillAlpha; } void SparklineItem::setLine2FillAlpha(qreal alpha) { if (qFuzzyCompare(m_line2FillAlpha, alpha)) return; m_line2FillAlpha = alpha; emit line2FillAlphaChanged(); update(); } qreal SparklineItem::maxValue() const { return m_maxValue; } void SparklineItem::setMaxValue(qreal value) { if (qFuzzyCompare(m_maxValue, value)) return; m_maxValue = value; emit maxValueChanged(); update(); } qreal SparklineItem::slideProgress() const { return m_slideProgress; } void SparklineItem::setSlideProgress(qreal progress) { if (qFuzzyCompare(m_slideProgress, progress)) return; m_slideProgress = progress; emit slideProgressChanged(); update(); } int SparklineItem::historyLength() const { return m_historyLength; } void SparklineItem::setHistoryLength(int length) { if (m_historyLength == length) return; m_historyLength = length; emit historyLengthChanged(); update(); } qreal SparklineItem::lineWidth() const { return m_lineWidth; } void SparklineItem::setLineWidth(qreal width) { if (qFuzzyCompare(m_lineWidth, width)) return; m_lineWidth = width; emit lineWidthChanged(); update(); } } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Internal/sparklineitem.hpp ================================================ #pragma once #include #include #include #include #include "circularbuffer.hpp" namespace caelestia::internal { class SparklineItem : public QQuickPaintedItem { Q_OBJECT QML_ELEMENT Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed) Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed) Q_PROPERTY(QColor line1Color READ line1Color WRITE setLine1Color NOTIFY line1ColorChanged) Q_PROPERTY(QColor line2Color READ line2Color WRITE setLine2Color NOTIFY line2ColorChanged) Q_PROPERTY(qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY line1FillAlphaChanged) Q_PROPERTY(qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY line2FillAlphaChanged) Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) Q_PROPERTY(qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY slideProgressChanged) Q_PROPERTY(int historyLength READ historyLength WRITE setHistoryLength NOTIFY historyLengthChanged) Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) public: explicit SparklineItem(QQuickItem* parent = nullptr); void paint(QPainter* painter) override; [[nodiscard]] CircularBuffer* line1() const; void setLine1(CircularBuffer* buffer); [[nodiscard]] CircularBuffer* line2() const; void setLine2(CircularBuffer* buffer); [[nodiscard]] QColor line1Color() const; void setLine1Color(const QColor& color); [[nodiscard]] QColor line2Color() const; void setLine2Color(const QColor& color); [[nodiscard]] qreal line1FillAlpha() const; void setLine1FillAlpha(qreal alpha); [[nodiscard]] qreal line2FillAlpha() const; void setLine2FillAlpha(qreal alpha); [[nodiscard]] qreal maxValue() const; void setMaxValue(qreal value); [[nodiscard]] qreal slideProgress() const; void setSlideProgress(qreal progress); [[nodiscard]] int historyLength() const; void setHistoryLength(int length); [[nodiscard]] qreal lineWidth() const; void setLineWidth(qreal width); signals: void line1Changed(); void line2Changed(); void line1ColorChanged(); void line2ColorChanged(); void line1FillAlphaChanged(); void line2FillAlphaChanged(); void maxValueChanged(); void slideProgressChanged(); void historyLengthChanged(); void lineWidthChanged(); private: void drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha); void connectBuffer(CircularBuffer* buffer); CircularBuffer* m_line1 = nullptr; CircularBuffer* m_line2 = nullptr; QColor m_line1Color; QColor m_line2Color; qreal m_line1FillAlpha = 0.15; qreal m_line2FillAlpha = 0.2; qreal m_maxValue = 1024.0; qreal m_slideProgress = 0.0; int m_historyLength = 30; qreal m_lineWidth = 2.0; }; } // namespace caelestia::internal ================================================ FILE: plugin/src/Caelestia/Models/CMakeLists.txt ================================================ qml_module(caelestia-models URI Caelestia.Models SOURCES filesystemmodel.hpp filesystemmodel.cpp LIBRARIES Qt::Gui Qt::Concurrent ) ================================================ FILE: plugin/src/Caelestia/Models/filesystemmodel.cpp ================================================ #include "filesystemmodel.hpp" #include #include #include namespace caelestia::models { FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent) : QObject(parent) , m_fileInfo(path) , m_path(path) , m_relativePath(relativePath) , m_isImageInitialised(false) , m_mimeTypeInitialised(false) {} QString FileSystemEntry::path() const { return m_path; }; QString FileSystemEntry::relativePath() const { return m_relativePath; }; QString FileSystemEntry::name() const { return m_fileInfo.fileName(); }; QString FileSystemEntry::baseName() const { return m_fileInfo.baseName(); }; QString FileSystemEntry::parentDir() const { return m_fileInfo.absolutePath(); }; QString FileSystemEntry::suffix() const { return m_fileInfo.completeSuffix(); }; qint64 FileSystemEntry::size() const { return m_fileInfo.size(); }; bool FileSystemEntry::isDir() const { return m_fileInfo.isDir(); }; bool FileSystemEntry::isImage() const { if (!m_isImageInitialised) { QImageReader reader(m_path); m_isImage = reader.canRead(); m_isImageInitialised = true; } return m_isImage; } QString FileSystemEntry::mimeType() const { if (!m_mimeTypeInitialised) { static const QMimeDatabase s_db; m_mimeType = s_db.mimeTypeForFile(m_path).name(); m_mimeTypeInitialised = true; } return m_mimeType; } void FileSystemEntry::updateRelativePath(const QDir& dir) { const auto relPath = dir.relativeFilePath(m_path); if (m_relativePath != relPath) { m_relativePath = relPath; emit relativePathChanged(); } } FileSystemModel::FileSystemModel(QObject* parent) : QAbstractListModel(parent) , m_recursive(false) , m_watchChanges(true) , m_showHidden(false) , m_filter(NoFilter) { connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir); } int FileSystemModel::rowCount(const QModelIndex& parent) const { if (parent != QModelIndex()) { return 0; } return static_cast(m_entries.size()); } QVariant FileSystemModel::data(const QModelIndex& index, int role) const { if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) { return QVariant(); } return QVariant::fromValue(m_entries.at(index.row())); } QHash FileSystemModel::roleNames() const { return { { Qt::UserRole, "modelData" } }; } QString FileSystemModel::path() const { return m_path; } void FileSystemModel::setPath(const QString& path) { if (m_path == path) { return; } m_path = path; emit pathChanged(); m_dir.setPath(m_path); for (const auto& entry : std::as_const(m_entries)) { entry->updateRelativePath(m_dir); } update(); } bool FileSystemModel::recursive() const { return m_recursive; } void FileSystemModel::setRecursive(bool recursive) { if (m_recursive == recursive) { return; } m_recursive = recursive; emit recursiveChanged(); update(); } bool FileSystemModel::watchChanges() const { return m_watchChanges; } void FileSystemModel::setWatchChanges(bool watchChanges) { if (m_watchChanges == watchChanges) { return; } m_watchChanges = watchChanges; emit watchChangesChanged(); update(); } bool FileSystemModel::showHidden() const { return m_showHidden; } void FileSystemModel::setShowHidden(bool showHidden) { if (m_showHidden == showHidden) { return; } m_showHidden = showHidden; emit showHiddenChanged(); update(); } bool FileSystemModel::sortReverse() const { return m_sortReverse; } void FileSystemModel::setSortReverse(bool sortReverse) { if (m_sortReverse == sortReverse) { return; } m_sortReverse = sortReverse; emit sortReverseChanged(); update(); } FileSystemModel::Filter FileSystemModel::filter() const { return m_filter; } void FileSystemModel::setFilter(Filter filter) { if (m_filter == filter) { return; } m_filter = filter; emit filterChanged(); update(); } QStringList FileSystemModel::nameFilters() const { return m_nameFilters; } void FileSystemModel::setNameFilters(const QStringList& nameFilters) { if (m_nameFilters == nameFilters) { return; } m_nameFilters = nameFilters; emit nameFiltersChanged(); update(); } QQmlListProperty FileSystemModel::entries() { return QQmlListProperty(this, &m_entries); } void FileSystemModel::watchDirIfRecursive(const QString& path) { if (m_recursive && m_watchChanges) { const auto currentDir = m_dir; const bool showHidden = m_showHidden; const auto future = QtConcurrent::run([showHidden, path]() { QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; if (showHidden) { filters |= QDir::Hidden; } QDirIterator iter(path, filters, QDirIterator::Subdirectories); QStringList dirs; while (iter.hasNext()) { dirs << iter.next(); } return dirs; }); const auto watcher = new QFutureWatcher(this); connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { const auto paths = watcher->result(); if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { // Ignore if dir or showHidden has changed m_watcher.addPaths(paths); } watcher->deleteLater(); }); watcher->setFuture(future); } } void FileSystemModel::update() { updateWatcher(); updateEntries(); } void FileSystemModel::updateWatcher() { if (!m_watcher.directories().isEmpty()) { m_watcher.removePaths(m_watcher.directories()); } if (!m_watchChanges || m_path.isEmpty()) { return; } m_watcher.addPath(m_path); watchDirIfRecursive(m_path); } void FileSystemModel::updateEntries() { if (m_path.isEmpty()) { if (!m_entries.isEmpty()) { beginResetModel(); qDeleteAll(m_entries); m_entries.clear(); endResetModel(); emit entriesChanged(); } return; } for (auto& future : m_futures) { future.cancel(); } m_futures.clear(); updateEntriesForDir(m_path); } void FileSystemModel::updateEntriesForDir(const QString& dir) { const auto recursive = m_recursive; const auto showHidden = m_showHidden; const auto filter = m_filter; const auto nameFilters = m_nameFilters; QSet oldPaths; for (const auto& entry : std::as_const(m_entries)) { oldPaths << entry->path(); } const auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; std::optional iter; if (filter == Images) { QStringList extraNameFilters = nameFilters; const auto formats = QImageReader::supportedImageFormats(); for (const auto& format : formats) { extraNameFilters << "*." + format; } QDir::Filters filters = QDir::Files; if (showHidden) { filters |= QDir::Hidden; } iter.emplace(dir, extraNameFilters, filters, flags); } else { QDir::Filters filters; if (filter == Files) { filters = QDir::Files; } else if (filter == Dirs) { filters = QDir::Dirs | QDir::NoDotAndDotDot; } else { filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot; } if (showHidden) { filters |= QDir::Hidden; } if (nameFilters.isEmpty()) { iter.emplace(dir, filters, flags); } else { iter.emplace(dir, nameFilters, filters, flags); } } QSet newPaths; while (iter->hasNext()) { if (promise.isCanceled()) { return; } QString path = iter->next(); if (filter == Images) { QImageReader reader(path); if (!reader.canRead()) { continue; } } newPaths.insert(path); } if (promise.isCanceled() || newPaths == oldPaths) { return; } promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths)); }); if (m_futures.contains(dir)) { m_futures[dir].cancel(); } m_futures.insert(dir, future); const auto watcher = new QFutureWatcher, QSet>>(this); connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { m_futures.remove(dir); if (!watcher->future().isResultReadyAt(0)) { watcher->deleteLater(); return; } const auto result = watcher->result(); applyChanges(result.first, result.second); watcher->deleteLater(); }); watcher->setFuture(future); } void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { QList removedIndices; for (int i = 0; i < m_entries.size(); ++i) { if (removedPaths.contains(m_entries[i]->path())) { removedIndices << i; } } std::sort(removedIndices.begin(), removedIndices.end(), std::greater()); // Batch remove old entries int start = -1; int end = -1; for (int idx : std::as_const(removedIndices)) { if (start == -1) { start = idx; end = idx; } else if (idx == end - 1) { end = idx; } else { beginRemoveRows(QModelIndex(), end, start); for (int i = start; i >= end; --i) { m_entries.takeAt(i)->deleteLater(); } endRemoveRows(); start = idx; end = idx; } } if (start != -1) { beginRemoveRows(QModelIndex(), end, start); for (int i = start; i >= end; --i) { m_entries.takeAt(i)->deleteLater(); } endRemoveRows(); } // Create new entries QList newEntries; for (const auto& path : addedPaths) { newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this); } std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) { return compareEntries(a, b); }); // Batch insert new entries int insertStart = -1; QList batchItems; for (const auto& entry : std::as_const(newEntries)) { const auto it = std::lower_bound( m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) { return compareEntries(a, b); }); const auto row = static_cast(it - m_entries.begin()); if (insertStart == -1) { insertStart = row; batchItems << entry; } else if (row == insertStart + batchItems.size()) { batchItems << entry; } else { beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast(batchItems.size()) - 1); for (int i = 0; i < batchItems.size(); ++i) { m_entries.insert(insertStart + i, batchItems[i]); } endInsertRows(); insertStart = row; batchItems.clear(); batchItems << entry; } } if (!batchItems.isEmpty()) { beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast(batchItems.size()) - 1); for (int i = 0; i < batchItems.size(); ++i) { m_entries.insert(insertStart + i, batchItems[i]); } endInsertRows(); } emit entriesChanged(); } bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const { if (a->isDir() != b->isDir()) { return m_sortReverse ^ a->isDir(); } const auto cmp = a->relativePath().localeAwareCompare(b->relativePath()); return m_sortReverse ? cmp > 0 : cmp < 0; } } // namespace caelestia::models ================================================ FILE: plugin/src/Caelestia/Models/filesystemmodel.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include #include namespace caelestia::models { class FileSystemEntry : public QObject { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel") Q_PROPERTY(QString path READ path CONSTANT) Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged) Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString baseName READ baseName CONSTANT) Q_PROPERTY(QString parentDir READ parentDir CONSTANT) Q_PROPERTY(QString suffix READ suffix CONSTANT) Q_PROPERTY(qint64 size READ size CONSTANT) Q_PROPERTY(bool isDir READ isDir CONSTANT) Q_PROPERTY(bool isImage READ isImage CONSTANT) Q_PROPERTY(QString mimeType READ mimeType CONSTANT) public: explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr); [[nodiscard]] QString path() const; [[nodiscard]] QString relativePath() const; [[nodiscard]] QString name() const; [[nodiscard]] QString baseName() const; [[nodiscard]] QString parentDir() const; [[nodiscard]] QString suffix() const; [[nodiscard]] qint64 size() const; [[nodiscard]] bool isDir() const; [[nodiscard]] bool isImage() const; [[nodiscard]] QString mimeType() const; void updateRelativePath(const QDir& dir); signals: void relativePathChanged(); private: const QFileInfo m_fileInfo; const QString m_path; QString m_relativePath; mutable bool m_isImage; mutable bool m_isImageInitialised; mutable QString m_mimeType; mutable bool m_mimeTypeInitialised; }; class FileSystemModel : public QAbstractListModel { Q_OBJECT QML_ELEMENT Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged) Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged) Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged) Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged) Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged) Q_PROPERTY(QQmlListProperty entries READ entries NOTIFY entriesChanged) public: enum Filter { NoFilter, Images, Files, Dirs }; Q_ENUM(Filter) explicit FileSystemModel(QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; [[nodiscard]] QString path() const; void setPath(const QString& path); [[nodiscard]] bool recursive() const; void setRecursive(bool recursive); [[nodiscard]] bool watchChanges() const; void setWatchChanges(bool watchChanges); [[nodiscard]] bool showHidden() const; void setShowHidden(bool showHidden); [[nodiscard]] bool sortReverse() const; void setSortReverse(bool sortReverse); [[nodiscard]] Filter filter() const; void setFilter(Filter filter); [[nodiscard]] QStringList nameFilters() const; void setNameFilters(const QStringList& nameFilters); [[nodiscard]] QQmlListProperty entries(); signals: void pathChanged(); void recursiveChanged(); void watchChangesChanged(); void showHiddenChanged(); void sortReverseChanged(); void filterChanged(); void nameFiltersChanged(); void entriesChanged(); private: QDir m_dir; QFileSystemWatcher m_watcher; QList m_entries; QHash, QSet>>> m_futures; QString m_path; bool m_recursive; bool m_watchChanges; bool m_showHidden; bool m_sortReverse; Filter m_filter; QStringList m_nameFilters; void watchDirIfRecursive(const QString& path); void update(); void updateWatcher(); void updateEntries(); void updateEntriesForDir(const QString& dir); void applyChanges(const QSet& removedPaths, const QSet& addedPaths); [[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const; }; } // namespace caelestia::models ================================================ FILE: plugin/src/Caelestia/Services/CMakeLists.txt ================================================ qml_module(caelestia-services URI Caelestia.Services SOURCES service.hpp service.cpp serviceref.hpp serviceref.cpp beattracker.hpp beattracker.cpp audiocollector.hpp audiocollector.cpp audioprovider.hpp audioprovider.cpp cavaprovider.hpp cavaprovider.cpp LIBRARIES PkgConfig::Pipewire PkgConfig::Aubio PkgConfig::Cava ) ================================================ FILE: plugin/src/Caelestia/Services/audiocollector.cpp ================================================ #include "audiocollector.hpp" #include "service.hpp" #include #include #include #include #include #include #include #include namespace caelestia::services { PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) : m_loop(nullptr) , m_stream(nullptr) , m_timer(nullptr) , m_idle(true) , m_token(token) , m_collector(collector) { pw_init(nullptr, nullptr); m_loop = pw_main_loop_new(nullptr); if (!m_loop) { qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop"; pw_deinit(); return; } timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC }; m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this); pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false); auto props = pw_properties_new( PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Music", nullptr); pw_properties_set(props, PW_KEY_STREAM_CAPTURE_SINK, "true"); pw_properties_setf( props, PW_KEY_NODE_LATENCY, "%u/%u", nextPowerOf2(512 * ac::SAMPLE_RATE / 48000), ac::SAMPLE_RATE); pw_properties_set(props, PW_KEY_NODE_PASSIVE, "true"); pw_properties_set(props, PW_KEY_NODE_VIRTUAL, "true"); pw_properties_set(props, PW_KEY_STREAM_DONT_REMIX, "false"); pw_properties_set(props, "channelmix.upmix", "true"); std::vector buffer(ac::CHUNK_SIZE); spa_pod_builder b; spa_pod_builder_init(&b, buffer.data(), static_cast(buffer.size())); spa_audio_info_raw info{}; info.format = SPA_AUDIO_FORMAT_S16; info.rate = ac::SAMPLE_RATE; info.channels = 1; const spa_pod* params[1]; params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); pw_stream_events events{}; events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) { auto* self = static_cast(data); self->streamStateChanged(state); }; events.process = [](void* data) { auto* self = static_cast(data); self->processStream(); }; m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this); const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY, static_cast( PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), params, 1); if (success < 0) { qWarning() << "PipeWireWorker::init: failed to connect stream"; pw_stream_destroy(m_stream); pw_main_loop_destroy(m_loop); pw_deinit(); return; } pw_main_loop_run(m_loop); pw_stream_destroy(m_stream); pw_main_loop_destroy(m_loop); pw_deinit(); } void PipeWireWorker::handleTimeout(void* data, uint64_t expirations) { auto* self = static_cast(data); if (self->m_token.stop_requested()) { pw_main_loop_quit(self->m_loop); return; } if (!self->m_idle) { if (expirations < 10) { self->m_collector->clearBuffer(); } else { self->m_idle = true; timespec timeout = { 0, 500 * SPA_NSEC_PER_MSEC }; pw_loop_update_timer(pw_main_loop_get_loop(self->m_loop), self->m_timer, &timeout, &timeout, false); } } } void PipeWireWorker::streamStateChanged(pw_stream_state state) { m_idle = false; switch (state) { case PW_STREAM_STATE_PAUSED: { timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC }; pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false); break; } case PW_STREAM_STATE_STREAMING: pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, nullptr, nullptr, false); break; case PW_STREAM_STATE_ERROR: pw_main_loop_quit(m_loop); break; default: break; } } void PipeWireWorker::processStream() { if (m_token.stop_requested()) { pw_main_loop_quit(m_loop); return; } pw_buffer* buffer = pw_stream_dequeue_buffer(m_stream); if (buffer == nullptr) { return; } const spa_buffer* buf = buffer->buffer; const qint16* samples = reinterpret_cast(buf->datas[0].data); if (samples == nullptr) { return; } const quint32 count = buf->datas[0].chunk->size / 2; m_collector->loadChunk(samples, count); pw_stream_queue_buffer(m_stream, buffer); } unsigned int PipeWireWorker::nextPowerOf2(unsigned int n) { if (n == 0) { return 1; } n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; n++; return n; } AudioCollector& AudioCollector::instance() { static AudioCollector instance; return instance; } void AudioCollector::clearBuffer() { auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed); std::fill(writeBuffer->begin(), writeBuffer->end(), 0.0f); auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel); m_writeBuffer.store(oldRead, std::memory_order_release); } void AudioCollector::loadChunk(const qint16* samples, quint32 count) { if (count > ac::CHUNK_SIZE) { count = ac::CHUNK_SIZE; } auto* writeBuffer = m_writeBuffer.load(std::memory_order_relaxed); std::transform(samples, samples + count, writeBuffer->begin(), [](qint16 sample) { return sample / 32768.0f; }); auto* oldRead = m_readBuffer.exchange(writeBuffer, std::memory_order_acq_rel); m_writeBuffer.store(oldRead, std::memory_order_release); } quint32 AudioCollector::readChunk(float* out, quint32 count) { if (count == 0 || count > ac::CHUNK_SIZE) { count = ac::CHUNK_SIZE; } auto* readBuffer = m_readBuffer.load(std::memory_order_acquire); std::memcpy(out, readBuffer->data(), count * sizeof(float)); return count; } quint32 AudioCollector::readChunk(double* out, quint32 count) { if (count == 0 || count > ac::CHUNK_SIZE) { count = ac::CHUNK_SIZE; } auto* readBuffer = m_readBuffer.load(std::memory_order_acquire); std::transform(readBuffer->begin(), readBuffer->begin() + count, out, [](float sample) { return static_cast(sample); }); return count; } AudioCollector::AudioCollector(QObject* parent) : Service(parent) , m_buffer1(ac::CHUNK_SIZE) , m_buffer2(ac::CHUNK_SIZE) , m_readBuffer(&m_buffer1) , m_writeBuffer(&m_buffer2) {} AudioCollector::~AudioCollector() { stop(); } void AudioCollector::start() { if (m_thread.joinable()) { return; } clearBuffer(); m_thread = std::jthread([this](std::stop_token token) { PipeWireWorker worker(token, this); }); } void AudioCollector::stop() { if (m_thread.joinable()) { m_thread.request_stop(); m_thread.join(); } } } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/audiocollector.hpp ================================================ #pragma once #include "service.hpp" #include #include #include #include #include #include #include #include namespace caelestia::services { namespace ac { constexpr quint32 SAMPLE_RATE = 44100; constexpr quint32 CHUNK_SIZE = 512; } // namespace ac class AudioCollector; class PipeWireWorker { public: explicit PipeWireWorker(std::stop_token token, AudioCollector* collector); void run(); private: pw_main_loop* m_loop; pw_stream* m_stream; spa_source* m_timer; bool m_idle; std::stop_token m_token; AudioCollector* m_collector; static void handleTimeout(void* data, uint64_t expirations); void streamStateChanged(pw_stream_state state); void processStream(); [[nodiscard]] unsigned int nextPowerOf2(unsigned int n); }; class AudioCollector : public Service { Q_OBJECT public: AudioCollector(const AudioCollector&) = delete; AudioCollector& operator=(const AudioCollector&) = delete; static AudioCollector& instance(); void clearBuffer(); void loadChunk(const qint16* samples, quint32 count); quint32 readChunk(float* out, quint32 count = 0); quint32 readChunk(double* out, quint32 count = 0); private: explicit AudioCollector(QObject* parent = nullptr); ~AudioCollector(); std::jthread m_thread; std::vector m_buffer1; std::vector m_buffer2; std::atomic*> m_readBuffer; std::atomic*> m_writeBuffer; quint32 m_sampleCount; void reload(); void start() override; void stop() override; }; } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/audioprovider.cpp ================================================ #include "audioprovider.hpp" #include "audiocollector.hpp" #include "service.hpp" #include #include namespace caelestia::services { AudioProcessor::AudioProcessor(QObject* parent) : QObject(parent) {} AudioProcessor::~AudioProcessor() { stop(); } void AudioProcessor::init() { m_timer = new QTimer(this); m_timer->setInterval(static_cast(ac::CHUNK_SIZE * 1000.0 / ac::SAMPLE_RATE)); connect(m_timer, &QTimer::timeout, this, &AudioProcessor::process); } void AudioProcessor::start() { QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::ref, Qt::QueuedConnection, this); if (m_timer) { m_timer->start(); } } void AudioProcessor::stop() { if (m_timer) { m_timer->stop(); } QMetaObject::invokeMethod(&AudioCollector::instance(), &AudioCollector::unref, Qt::QueuedConnection, this); } AudioProvider::AudioProvider(QObject* parent) : Service(parent) , m_processor(nullptr) , m_thread(nullptr) {} AudioProvider::~AudioProvider() { if (m_thread) { m_thread->quit(); m_thread->wait(); } } void AudioProvider::init() { if (!m_processor) { qWarning() << "AudioProvider::init: attempted to init with no processor set"; return; } m_thread = new QThread(this); m_processor->moveToThread(m_thread); connect(m_thread, &QThread::started, m_processor, &AudioProcessor::init); connect(m_thread, &QThread::finished, m_processor, &AudioProcessor::deleteLater); connect(m_thread, &QThread::finished, m_thread, &QThread::deleteLater); m_thread->start(); } void AudioProvider::start() { if (m_processor) { AudioCollector::instance(); // Create instance on main thread QMetaObject::invokeMethod(m_processor, &AudioProcessor::start); } } void AudioProvider::stop() { if (m_processor) { QMetaObject::invokeMethod(m_processor, &AudioProcessor::stop); } } } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/audioprovider.hpp ================================================ #pragma once #include "service.hpp" #include #include namespace caelestia::services { class AudioProcessor : public QObject { Q_OBJECT public: explicit AudioProcessor(QObject* parent = nullptr); ~AudioProcessor(); void init(); public slots: void start(); void stop(); protected: virtual void process() = 0; private: QTimer* m_timer = nullptr; }; class AudioProvider : public Service { Q_OBJECT public: explicit AudioProvider(QObject* parent = nullptr); ~AudioProvider(); protected: AudioProcessor* m_processor; void init(); private: QThread* m_thread; void start() override; void stop() override; }; } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/beattracker.cpp ================================================ #include "beattracker.hpp" #include "audiocollector.hpp" #include "audioprovider.hpp" #include namespace caelestia::services { BeatProcessor::BeatProcessor(QObject* parent) : AudioProcessor(parent) , m_tempo(new_aubio_tempo("default", 1024, ac::CHUNK_SIZE, ac::SAMPLE_RATE)) , m_in(new_fvec(ac::CHUNK_SIZE)) , m_out(new_fvec(2)) {}; BeatProcessor::~BeatProcessor() { if (m_tempo) { del_aubio_tempo(m_tempo); } if (m_in) { del_fvec(m_in); } if (m_out) { del_fvec(m_out); } } void BeatProcessor::process() { if (!m_tempo || !m_in) { return; } AudioCollector::instance().readChunk(m_in->data); aubio_tempo_do(m_tempo, m_in, m_out); if (!qFuzzyIsNull(m_out->data[0])) { emit beat(aubio_tempo_get_bpm(m_tempo)); } } BeatTracker::BeatTracker(QObject* parent) : AudioProvider(parent) , m_bpm(120) { m_processor = new BeatProcessor(); init(); connect(static_cast(m_processor), &BeatProcessor::beat, this, &BeatTracker::updateBpm); } smpl_t BeatTracker::bpm() const { return m_bpm; } void BeatTracker::updateBpm(smpl_t bpm) { if (!qFuzzyCompare(bpm + 1.0f, m_bpm + 1.0f)) { m_bpm = bpm; emit bpmChanged(); } } } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/beattracker.hpp ================================================ #pragma once #include "audioprovider.hpp" #include #include namespace caelestia::services { class BeatProcessor : public AudioProcessor { Q_OBJECT public: explicit BeatProcessor(QObject* parent = nullptr); ~BeatProcessor(); signals: void beat(smpl_t bpm); protected: void process() override; private: aubio_tempo_t* m_tempo; fvec_t* m_in; fvec_t* m_out; }; class BeatTracker : public AudioProvider { Q_OBJECT QML_ELEMENT Q_PROPERTY(smpl_t bpm READ bpm NOTIFY bpmChanged) public: explicit BeatTracker(QObject* parent = nullptr); [[nodiscard]] smpl_t bpm() const; signals: void bpmChanged(); void beat(smpl_t bpm); private: smpl_t m_bpm; void updateBpm(smpl_t bpm); }; } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/cavaprovider.cpp ================================================ #include "cavaprovider.hpp" #include "audiocollector.hpp" #include "audioprovider.hpp" #include #include #include namespace caelestia::services { CavaProcessor::CavaProcessor(QObject* parent) : AudioProcessor(parent) , m_plan(nullptr) , m_in(new double[ac::CHUNK_SIZE]) , m_out(nullptr) , m_bars(0) {}; CavaProcessor::~CavaProcessor() { cleanup(); delete[] m_in; } void CavaProcessor::process() { if (!m_plan || m_bars == 0 || !m_out) { return; } const int count = static_cast(AudioCollector::instance().readChunk(m_in)); // Process in data via cava cava_execute(m_in, count, m_out, m_plan); // Apply monstercat filter QVector values(m_bars); // Left to right pass const double inv = 1.0 / 1.5; double carry = 0.0; for (int i = 0; i < m_bars; ++i) { carry = std::max(m_out[i], carry * inv); values[i] = carry; } // Right to left pass and combine carry = 0.0; for (int i = m_bars - 1; i >= 0; --i) { carry = std::max(m_out[i], carry * inv); values[i] = std::max(values[i], carry); } // Update values if (values != m_values) { m_values = std::move(values); emit valuesChanged(m_values); } } void CavaProcessor::setBars(int bars) { if (bars < 0) { qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0."; bars = 0; } if (m_bars != bars) { m_bars = bars; reload(); } } void CavaProcessor::reload() { cleanup(); initCava(); } void CavaProcessor::cleanup() { if (m_plan) { cava_destroy(m_plan); m_plan = nullptr; } if (m_out) { delete[] m_out; m_out = nullptr; } } void CavaProcessor::initCava() { if (m_plan || m_bars == 0) { return; } m_plan = cava_init(m_bars, ac::SAMPLE_RATE, 1, 1, 0.85, 50, 10000); m_out = new double[static_cast(m_bars)]; } CavaProvider::CavaProvider(QObject* parent) : AudioProvider(parent) , m_bars(0) , m_values(m_bars, 0.0) { m_processor = new CavaProcessor(); init(); connect(static_cast(m_processor), &CavaProcessor::valuesChanged, this, &CavaProvider::updateValues); } int CavaProvider::bars() const { return m_bars; } void CavaProvider::setBars(int bars) { if (bars < 0) { qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0."; bars = 0; } if (m_bars == bars) { return; } m_values.resize(bars, 0.0); m_bars = bars; emit barsChanged(); emit valuesChanged(); QMetaObject::invokeMethod( static_cast(m_processor), &CavaProcessor::setBars, Qt::QueuedConnection, bars); } QVector CavaProvider::values() const { return m_values; } void CavaProvider::updateValues(QVector values) { if (values != m_values) { m_values = values; emit valuesChanged(); } } } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/cavaprovider.hpp ================================================ #pragma once #include "audioprovider.hpp" #include #include namespace caelestia::services { class CavaProcessor : public AudioProcessor { Q_OBJECT public: explicit CavaProcessor(QObject* parent = nullptr); ~CavaProcessor(); void setBars(int bars); signals: void valuesChanged(QVector values); protected: void process() override; private: struct cava_plan* m_plan; double* m_in; double* m_out; int m_bars; QVector m_values; void reload(); void initCava(); void cleanup(); }; class CavaProvider : public AudioProvider { Q_OBJECT QML_ELEMENT Q_PROPERTY(int bars READ bars WRITE setBars NOTIFY barsChanged) Q_PROPERTY(QVector values READ values NOTIFY valuesChanged) public: explicit CavaProvider(QObject* parent = nullptr); [[nodiscard]] int bars() const; void setBars(int bars); [[nodiscard]] QVector values() const; signals: void barsChanged(); void valuesChanged(); private: int m_bars; QVector m_values; void updateValues(QVector values); }; } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/service.cpp ================================================ #include "service.hpp" #include #include namespace caelestia::services { Service::Service(QObject* parent) : QObject(parent) {} void Service::ref(QObject* sender) { if (m_refs.isEmpty()) { start(); } QObject::connect(sender, &QObject::destroyed, this, &Service::unref); m_refs << sender; } void Service::unref(QObject* sender) { if (m_refs.remove(sender) && m_refs.isEmpty()) { stop(); } } } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/service.hpp ================================================ #pragma once #include #include namespace caelestia::services { class Service : public QObject { Q_OBJECT public: explicit Service(QObject* parent = nullptr); void ref(QObject* sender); void unref(QObject* sender); private: QSet m_refs; virtual void start() = 0; virtual void stop() = 0; }; } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/serviceref.cpp ================================================ #include "serviceref.hpp" #include "service.hpp" namespace caelestia::services { ServiceRef::ServiceRef(Service* service, QObject* parent) : QObject(parent) , m_service(service) { if (m_service) { m_service->ref(this); } } Service* ServiceRef::service() const { return m_service; } void ServiceRef::setService(Service* service) { if (m_service == service) { return; } if (m_service) { m_service->unref(this); } m_service = service; emit serviceChanged(); if (m_service) { m_service->ref(this); } } } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/Services/serviceref.hpp ================================================ #pragma once #include "service.hpp" #include #include namespace caelestia::services { class ServiceRef : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(caelestia::services::Service* service READ service WRITE setService NOTIFY serviceChanged) public: explicit ServiceRef(Service* service = nullptr, QObject* parent = nullptr); [[nodiscard]] Service* service() const; void setService(Service* service); signals: void serviceChanged(); private: QPointer m_service; }; } // namespace caelestia::services ================================================ FILE: plugin/src/Caelestia/appdb.cpp ================================================ #include "appdb.hpp" #include #include #include namespace caelestia { AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) : QObject(parent) , m_entry(entry) , m_frequency(frequency) { const auto mo = m_entry->metaObject(); const auto tmo = metaObject(); for (const auto& prop : { "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) { const auto metaProp = mo->property(mo->indexOfProperty(prop)); const auto thisMetaProp = tmo->property(tmo->indexOfProperty(prop)); QObject::connect(m_entry, metaProp.notifySignal(), this, thisMetaProp.notifySignal()); } QObject::connect(m_entry, &QObject::destroyed, this, [this]() { m_entry = nullptr; deleteLater(); }); } QObject* AppEntry::entry() const { return m_entry; } quint32 AppEntry::frequency() const { return m_frequency; } void AppEntry::setFrequency(unsigned int frequency) { if (m_frequency != frequency) { m_frequency = frequency; emit frequencyChanged(); } } void AppEntry::incrementFrequency() { m_frequency++; emit frequencyChanged(); } QString AppEntry::id() const { if (!m_entry) { return ""; } return m_entry->property("id").toString(); } QString AppEntry::name() const { if (!m_entry) { return ""; } return m_entry->property("name").toString(); } QString AppEntry::comment() const { if (!m_entry) { return ""; } return m_entry->property("comment").toString(); } QString AppEntry::execString() const { if (!m_entry) { return ""; } return m_entry->property("execString").toString(); } QString AppEntry::startupClass() const { if (!m_entry) { return ""; } return m_entry->property("startupClass").toString(); } QString AppEntry::genericName() const { if (!m_entry) { return ""; } return m_entry->property("genericName").toString(); } QString AppEntry::categories() const { if (!m_entry) { return ""; } return m_entry->property("categories").toStringList().join(" "); } QString AppEntry::keywords() const { if (!m_entry) { return ""; } return m_entry->property("keywords").toStringList().join(" "); } AppDb::AppDb(QObject* parent) : QObject(parent) , m_timer(new QTimer(this)) , m_uuid(QUuid::createUuid().toString()) { m_timer->setSingleShot(true); m_timer->setInterval(300); QObject::connect(m_timer, &QTimer::timeout, this, &AppDb::updateApps); auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid); db.setDatabaseName(":memory:"); db.open(); QSqlQuery query(db); query.exec("CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)"); } QString AppDb::uuid() const { return m_uuid; } QString AppDb::path() const { return m_path; } void AppDb::setPath(const QString& path) { auto newPath = path.isEmpty() ? ":memory:" : path; if (m_path == newPath) { return; } m_path = newPath; emit pathChanged(); auto db = QSqlDatabase::database(m_uuid, false); db.close(); db.setDatabaseName(newPath); db.open(); QSqlQuery query(db); query.exec("CREATE TABLE IF NOT EXISTS frequencies (id TEXT PRIMARY KEY, frequency INTEGER)"); updateAppFrequencies(); } QObjectList AppDb::entries() const { return m_entries; } void AppDb::setEntries(const QObjectList& entries) { if (m_entries == entries) { return; } m_entries = entries; emit entriesChanged(); m_timer->start(); } QStringList AppDb::favouriteApps() const { return m_favouriteApps; } void AppDb::setFavouriteApps(const QStringList& favApps) { if (m_favouriteApps == favApps) { return; } m_favouriteApps = favApps; emit favouriteAppsChanged(); m_favouriteAppsRegex.clear(); m_favouriteAppsRegex.reserve(m_favouriteApps.size()); for (const QString& item : std::as_const(m_favouriteApps)) { const QRegularExpression re(regexifyString(item)); if (re.isValid()) { m_favouriteAppsRegex << re; } else { qWarning() << "AppDb::setFavouriteApps: Regular expression is not valid: " << re.pattern(); } } emit appsChanged(); } QString AppDb::regexifyString(const QString& original) const { if (original.startsWith('^') && original.endsWith('$')) return original; const QString escaped = QRegularExpression::escape(original); return QStringLiteral("^%1$").arg(escaped); } QQmlListProperty AppDb::apps() { return QQmlListProperty(this, &getSortedApps()); } void AppDb::incrementFrequency(const QString& id) { auto db = QSqlDatabase::database(m_uuid); QSqlQuery query(db); query.prepare("INSERT INTO frequencies (id, frequency) " "VALUES (:id, 1) " "ON CONFLICT (id) DO UPDATE SET frequency = frequency + 1"); query.bindValue(":id", id); query.exec(); auto* app = m_apps.value(id); if (app) { const auto before = getSortedApps(); app->incrementFrequency(); getSortedApps(); if (before != m_sortedApps) { emit appsChanged(); } } else { qWarning() << "AppDb::incrementFrequency: could not find app with id" << id; } } QList& AppDb::getSortedApps() const { m_sortedApps = m_apps.values(); // Pre-compute favourite status to avoid repeated regex matching during sort QSet favSet; favSet.reserve(m_sortedApps.size()); for (const auto* app : std::as_const(m_sortedApps)) { if (isFavourite(app)) favSet.insert(app->id()); } std::sort(m_sortedApps.begin(), m_sortedApps.end(), [&favSet](AppEntry* a, AppEntry* b) { const bool aIsFav = favSet.contains(a->id()); const bool bIsFav = favSet.contains(b->id()); if (aIsFav != bIsFav) return aIsFav; if (a->frequency() != b->frequency()) return a->frequency() > b->frequency(); return a->name().localeAwareCompare(b->name()) < 0; }); return m_sortedApps; } bool AppDb::isFavourite(const AppEntry* app) const { for (const QRegularExpression& re : m_favouriteAppsRegex) { if (re.match(app->id()).hasMatch()) { return true; } } return false; } quint32 AppDb::getFrequency(const QString& id) const { auto db = QSqlDatabase::database(m_uuid); QSqlQuery query(db); query.prepare("SELECT frequency FROM frequencies WHERE id = :id"); query.bindValue(":id", id); if (query.exec() && query.next()) { return query.value(0).toUInt(); } return 0; } void AppDb::updateAppFrequencies() { const auto before = getSortedApps(); for (auto* app : std::as_const(m_apps)) { app->setFrequency(getFrequency(app->id())); } getSortedApps(); if (before != m_sortedApps) { emit appsChanged(); } } void AppDb::updateApps() { bool dirty = false; for (const auto& entry : std::as_const(m_entries)) { const auto id = entry->property("id").toString(); if (!m_apps.contains(id)) { dirty = true; auto* const newEntry = new AppEntry(entry, getFrequency(id), this); QObject::connect(newEntry, &QObject::destroyed, this, [id, this]() { if (m_apps.remove(id)) { emit appsChanged(); } }); m_apps.insert(id, newEntry); } } QSet newIds; for (const auto& entry : std::as_const(m_entries)) { newIds.insert(entry->property("id").toString()); } for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) { const auto& id = *it; if (!newIds.contains(id)) { dirty = true; m_apps.take(id)->deleteLater(); } } if (dirty) { emit appsChanged(); } } } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/appdb.hpp ================================================ #pragma once #include #include #include #include #include #include namespace caelestia { class AppEntry : public QObject { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("AppEntry instances can only be retrieved from an AppDb") // The actual DesktopEntry, but we don't have access to the type so it's a QObject Q_PROPERTY(QObject* entry READ entry CONSTANT) Q_PROPERTY(quint32 frequency READ frequency NOTIFY frequencyChanged) Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString comment READ comment NOTIFY commentChanged) Q_PROPERTY(QString execString READ execString NOTIFY execStringChanged) Q_PROPERTY(QString startupClass READ startupClass NOTIFY startupClassChanged) Q_PROPERTY(QString genericName READ genericName NOTIFY genericNameChanged) Q_PROPERTY(QString categories READ categories NOTIFY categoriesChanged) Q_PROPERTY(QString keywords READ keywords NOTIFY keywordsChanged) public: explicit AppEntry(QObject* entry, quint32 frequency, QObject* parent = nullptr); [[nodiscard]] QObject* entry() const; [[nodiscard]] quint32 frequency() const; void setFrequency(quint32 frequency); void incrementFrequency(); [[nodiscard]] QString id() const; [[nodiscard]] QString name() const; [[nodiscard]] QString comment() const; [[nodiscard]] QString execString() const; [[nodiscard]] QString startupClass() const; [[nodiscard]] QString genericName() const; [[nodiscard]] QString categories() const; [[nodiscard]] QString keywords() const; signals: void frequencyChanged(); void nameChanged(); void commentChanged(); void execStringChanged(); void startupClassChanged(); void genericNameChanged(); void categoriesChanged(); void keywordsChanged(); private: QObject* m_entry; quint32 m_frequency; }; class AppDb : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(QString uuid READ uuid CONSTANT) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED) Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED) Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED) Q_PROPERTY(QQmlListProperty apps READ apps NOTIFY appsChanged) public: explicit AppDb(QObject* parent = nullptr); [[nodiscard]] QString uuid() const; [[nodiscard]] QString path() const; void setPath(const QString& path); [[nodiscard]] QObjectList entries() const; void setEntries(const QObjectList& entries); [[nodiscard]] QStringList favouriteApps() const; void setFavouriteApps(const QStringList& favApps); [[nodiscard]] QQmlListProperty apps(); Q_INVOKABLE void incrementFrequency(const QString& id); signals: void pathChanged(); void entriesChanged(); void favouriteAppsChanged(); void appsChanged(); private: QTimer* m_timer; const QString m_uuid; QString m_path; QObjectList m_entries; QStringList m_favouriteApps; // unedited string list from qml QList m_favouriteAppsRegex; // pre-regexified m_favouriteApps list QHash m_apps; mutable QList m_sortedApps; QString regexifyString(const QString& original) const; QList& getSortedApps() const; bool isFavourite(const AppEntry* app) const; quint32 getFrequency(const QString& id) const; void updateAppFrequencies(); void updateApps(); }; } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/cutils.cpp ================================================ #include "cutils.hpp" #include #include #include #include #include #include #include namespace caelestia { void CUtils::saveItem(QQuickItem* target, const QUrl& path) { this->saveItem(target, path, QRect(), QJSValue(), QJSValue()); } void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) { this->saveItem(target, path, rect, QJSValue(), QJSValue()); } void CUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) { this->saveItem(target, path, QRect(), onSaved, QJSValue()); } void CUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) { this->saveItem(target, path, QRect(), onSaved, onFailed); } void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) { this->saveItem(target, path, rect, onSaved, QJSValue()); } void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { if (!target) { qWarning() << "CUtils::saveItem: a target is required"; return; } if (!path.isLocalFile()) { qWarning() << "CUtils::saveItem:" << path << "is not a local file"; return; } if (!target->window()) { qWarning() << "CUtils::saveItem: unable to save target" << target << "without a window"; return; } auto scaledRect = rect; const qreal scale = target->window()->devicePixelRatio(); if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) { scaledRect = QRectF(rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale).toRect(); } const QSharedPointer grabResult = target->grabToImage(); QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, scaledRect, path, onSaved, onFailed, this]() { const auto future = QtConcurrent::run([=]() { QImage image = grabResult->image(); if (scaledRect.isValid()) { image = image.copy(scaledRect); } const QString file = path.toLocalFile(); const QString parent = QFileInfo(file).absolutePath(); return QDir().mkpath(parent) && image.save(file); }); auto* watcher = new QFutureWatcher(this); auto* engine = qmlEngine(this); QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { if (watcher->result()) { if (onSaved.isCallable()) { onSaved.call( { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); } } else { qWarning() << "CUtils::saveItem: failed to save" << path; if (onFailed.isCallable()) { onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); } } watcher->deleteLater(); }); watcher->setFuture(future); }); } bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { if (!source.isLocalFile()) { qWarning() << "CUtils::copyFile: source" << source << "is not a local file"; return false; } if (!target.isLocalFile()) { qWarning() << "CUtils::copyFile: target" << target << "is not a local file"; return false; } if (overwrite && QFile::exists(target.toLocalFile())) { if (!QFile::remove(target.toLocalFile())) { qWarning() << "CUtils::copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); return false; } } return QFile::copy(source.toLocalFile(), target.toLocalFile()); } bool CUtils::deleteFile(const QUrl& path) const { if (!path.isLocalFile()) { qWarning() << "CUtils::deleteFile: path" << path << "is not a local file"; return false; } return QFile::remove(path.toLocalFile()); } QString CUtils::toLocalFile(const QUrl& url) const { if (!url.isLocalFile()) { qWarning() << "CUtils::toLocalFile: given url is not a local file" << url; return QString(); } return url.toLocalFile(); } } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/cutils.hpp ================================================ #pragma once #include #include #include namespace caelestia { class CUtils : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON public: // clang-format off Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); // clang-format on Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; Q_INVOKABLE bool deleteFile(const QUrl& path) const; Q_INVOKABLE QString toLocalFile(const QUrl& url) const; }; } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/imageanalyser.cpp ================================================ #include "imageanalyser.hpp" #include #include #include #include #include namespace caelestia { ImageAnalyser::ImageAnalyser(QObject* parent) : QObject(parent) , m_futureWatcher(new QFutureWatcher(this)) , m_source("") , m_sourceItem(nullptr) , m_rescaleSize(128) , m_dominantColour(0, 0, 0) , m_luminance(0) { QObject::connect(m_futureWatcher, &QFutureWatcher::finished, this, [this]() { if (!m_futureWatcher->future().isResultReadyAt(0)) { return; } const auto result = m_futureWatcher->result(); if (m_dominantColour != result.first) { m_dominantColour = result.first; emit dominantColourChanged(); } if (!qFuzzyCompare(m_luminance + 1.0, result.second + 1.0)) { m_luminance = result.second; emit luminanceChanged(); } }); } QString ImageAnalyser::source() const { return m_source; } void ImageAnalyser::setSource(const QString& source) { if (m_source == source) { return; } m_source = source; emit sourceChanged(); if (m_sourceItem) { m_sourceItem = nullptr; emit sourceItemChanged(); } requestUpdate(); } QQuickItem* ImageAnalyser::sourceItem() const { return m_sourceItem; } void ImageAnalyser::setSourceItem(QQuickItem* sourceItem) { if (m_sourceItem == sourceItem) { return; } m_sourceItem = sourceItem; emit sourceItemChanged(); if (!m_source.isEmpty()) { m_source = ""; emit sourceChanged(); } requestUpdate(); } int ImageAnalyser::rescaleSize() const { return m_rescaleSize; } void ImageAnalyser::setRescaleSize(int rescaleSize) { if (m_rescaleSize == rescaleSize) { return; } m_rescaleSize = rescaleSize; emit rescaleSizeChanged(); requestUpdate(); } QColor ImageAnalyser::dominantColour() const { return m_dominantColour; } qreal ImageAnalyser::luminance() const { return m_luminance; } void ImageAnalyser::requestUpdate() { if (m_source.isEmpty() && !m_sourceItem) { return; } if (!m_sourceItem || (m_sourceItem->window() && m_sourceItem->window()->isVisible() && m_sourceItem->width() > 0 && m_sourceItem->height() > 0)) { update(); } else if (m_sourceItem) { if (!m_sourceItem->window()) { QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection); } else if (!m_sourceItem->window()->isVisible()) { QObject::connect(m_sourceItem->window(), &QQuickWindow::visibleChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection); } if (m_sourceItem->width() <= 0) { QObject::connect( m_sourceItem, &QQuickItem::widthChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection); } if (m_sourceItem->height() <= 0) { QObject::connect(m_sourceItem, &QQuickItem::heightChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection); } } } void ImageAnalyser::update() { if (m_source.isEmpty() && !m_sourceItem) { return; } if (m_futureWatcher->isRunning()) { m_futureWatcher->cancel(); } if (m_sourceItem) { const QSharedPointer grabResult = m_sourceItem->grabToImage(); QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); }); } else { m_futureWatcher->setFuture(QtConcurrent::run([=, this](QPromise& promise) { const QImage image(m_source); analyse(promise, image, m_rescaleSize); })); } } void ImageAnalyser::analyse(QPromise& promise, const QImage& image, int rescaleSize) { if (image.isNull()) { qWarning() << "ImageAnalyser::analyse: image is null"; return; } QImage img = image; if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) { img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation); } if (promise.isCanceled()) { return; } if (img.format() != QImage::Format_ARGB32) { img = img.convertToFormat(QImage::Format_ARGB32); } if (promise.isCanceled()) { return; } const uchar* data = img.bits(); const int width = img.width(); const int height = img.height(); const qsizetype bytesPerLine = img.bytesPerLine(); std::unordered_map colours; qreal totalLuminance = 0.0; int count = 0; for (int y = 0; y < height; ++y) { const uchar* line = data + y * bytesPerLine; for (int x = 0; x < width; ++x) { if (promise.isCanceled()) { return; } const uchar* pixel = line + x * 4; if (pixel[3] == 0) { continue; } const quint32 mr = static_cast(pixel[2] & 0xF8); const quint32 mg = static_cast(pixel[1] & 0xF8); const quint32 mb = static_cast(pixel[0] & 0xF8); ++colours[(mr << 16) | (mg << 8) | mb]; const qreal r = pixel[2] / 255.0; const qreal g = pixel[1] / 255.0; const qreal b = pixel[0] / 255.0; totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); ++count; } } quint32 dominantColour = 0; int maxCount = 0; for (const auto& [colour, colourCount] : colours) { if (promise.isCanceled()) { return; } if (colourCount > maxCount) { dominantColour = colour; maxCount = colourCount; } } promise.addResult(qMakePair(QColor((0xFFu << 24) | dominantColour), count == 0 ? 0.0 : totalLuminance / count)); } } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/imageanalyser.hpp ================================================ #pragma once #include #include #include #include #include namespace caelestia { class ImageAnalyser : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(QQuickItem* sourceItem READ sourceItem WRITE setSourceItem NOTIFY sourceItemChanged) Q_PROPERTY(int rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged) Q_PROPERTY(QColor dominantColour READ dominantColour NOTIFY dominantColourChanged) Q_PROPERTY(qreal luminance READ luminance NOTIFY luminanceChanged) public: explicit ImageAnalyser(QObject* parent = nullptr); [[nodiscard]] QString source() const; void setSource(const QString& source); [[nodiscard]] QQuickItem* sourceItem() const; void setSourceItem(QQuickItem* sourceItem); [[nodiscard]] int rescaleSize() const; void setRescaleSize(int rescaleSize); [[nodiscard]] QColor dominantColour() const; [[nodiscard]] qreal luminance() const; Q_INVOKABLE void requestUpdate(); signals: void sourceChanged(); void sourceItemChanged(); void rescaleSizeChanged(); void dominantColourChanged(); void luminanceChanged(); private: using AnalyseResult = QPair; QFutureWatcher* const m_futureWatcher; QString m_source; QQuickItem* m_sourceItem; int m_rescaleSize; QColor m_dominantColour; qreal m_luminance; void update(); static void analyse(QPromise& promise, const QImage& image, int rescaleSize); }; } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/qalculator.cpp ================================================ #include "qalculator.hpp" #include #include #include namespace caelestia { QMutex Qalculator::s_calculatorMutex; Qalculator::Qalculator(QObject* parent) : QObject(parent) { if (!CALCULATOR) { new Calculator(); CALCULATOR->loadExchangeRates(); CALCULATOR->loadGlobalDefinitions(); CALCULATOR->loadLocalDefinitions(); } } QString Qalculator::eval(const QString& expr, bool printExpr) const { if (expr.isEmpty()) { return QString(); } QMutexLocker locker(&s_calculatorMutex); EvaluationOptions eo; PrintOptions po; std::string parsed; std::string result = CALCULATOR->calculateAndPrint( CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); std::string error; while (CALCULATOR->message()) { if (!CALCULATOR->message()->message().empty()) { if (CALCULATOR->message()->type() == MESSAGE_ERROR) { error += "error: "; } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { error += "warning: "; } error += CALCULATOR->message()->message(); } CALCULATOR->nextMessage(); } if (!error.empty()) { return QString::fromStdString(error); } if (printExpr) { return QString("%1 = %2").arg(parsed).arg(result); } return QString::fromStdString(result); } void Qalculator::evalAsync(const QString& expr) { const quint64 gen = ++m_generation; if (expr.isEmpty()) { if (!m_result.isEmpty()) { m_result.clear(); emit resultChanged(); } if (!m_rawResult.isEmpty()) { m_rawResult.clear(); emit rawResultChanged(); } if (m_busy) { m_busy = false; emit busyChanged(); } return; } if (!m_busy) { m_busy = true; emit busyChanged(); } const auto future = QtConcurrent::run([expr]() -> QPair { QMutexLocker locker(&s_calculatorMutex); EvaluationOptions eo; PrintOptions po; std::string parsed; std::string result = CALCULATOR->calculateAndPrint( CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); std::string error; while (CALCULATOR->message()) { if (!CALCULATOR->message()->message().empty()) { if (CALCULATOR->message()->type() == MESSAGE_ERROR) { error += "error: "; } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { error += "warning: "; } error += CALCULATOR->message()->message(); } CALCULATOR->nextMessage(); } if (!error.empty()) { const QString errorStr = QString::fromStdString(error); return { errorStr, errorStr }; } const QString rawStr = QString::fromStdString(result); return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; }); auto* watcher = new QFutureWatcher>(this); connect(watcher, &QFutureWatcher>::finished, this, [this, watcher, gen]() { watcher->deleteLater(); if (gen != m_generation) { return; } const auto [formatted, raw] = watcher->result(); if (m_result != formatted) { m_result = formatted; emit resultChanged(); } if (m_rawResult != raw) { m_rawResult = raw; emit rawResultChanged(); } if (m_busy) { m_busy = false; emit busyChanged(); } }); watcher->setFuture(future); } QString Qalculator::result() const { return m_result; } QString Qalculator::rawResult() const { return m_rawResult; } bool Qalculator::busy() const { return m_busy; } } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/qalculator.hpp ================================================ #pragma once #include #include #include namespace caelestia { class Qalculator : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON Q_PROPERTY(QString result READ result NOTIFY resultChanged) Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: explicit Qalculator(QObject* parent = nullptr); Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; Q_INVOKABLE void evalAsync(const QString& expr); [[nodiscard]] QString result() const; [[nodiscard]] QString rawResult() const; [[nodiscard]] bool busy() const; signals: void resultChanged(); void rawResultChanged(); void busyChanged(); private: static QMutex s_calculatorMutex; QString m_result; QString m_rawResult; bool m_busy = false; quint64 m_generation = 0; }; } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/requests.cpp ================================================ #include "requests.hpp" #include #include #include #include #include namespace caelestia { Requests::Requests(QObject* parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) {} void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { if (!onSuccess.isCallable()) { qWarning() << "Requests::get: onSuccess is not callable"; return; } QNetworkRequest request(url); request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); request.setRawHeader("Cache-Control", "no-cache, no-store"); request.setRawHeader("Pragma", "no-cache"); request.setRawHeader("Connection", "close"); if (headers.isObject()) { QJSValueIterator it(headers); while (it.hasNext()) { it.next(); request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8()); } } auto reply = m_manager->get(request); QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() { if (reply->error() == QNetworkReply::NoError) { onSuccess.call({ QString(reply->readAll()) }); } else if (onError.isCallable()) { onError.call({ reply->errorString() }); } else { qWarning() << "Requests::get: request failed with error" << reply->errorString(); } reply->deleteLater(); }); } void Requests::resetCookies() const { m_manager->setCookieJar(new QNetworkCookieJar(m_manager)); } } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/requests.hpp ================================================ #pragma once #include #include #include namespace caelestia { class Requests : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON public: explicit Requests(QObject* parent = nullptr); Q_INVOKABLE void get( const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; Q_INVOKABLE void resetCookies() const; private: QNetworkAccessManager* m_manager; }; } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/toaster.cpp ================================================ #include "toaster.hpp" #include #include #include namespace caelestia { Toast::Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout, QObject* parent) : QObject(parent) , m_closed(false) , m_title(title) , m_message(message) , m_icon(icon) , m_type(type) , m_timeout(timeout) { QTimer::singleShot(timeout, this, &Toast::close); if (m_icon.isEmpty()) { switch (m_type) { case Type::Success: m_icon = "check_circle_unread"; break; case Type::Warning: m_icon = "warning"; break; case Type::Error: m_icon = "error"; break; default: m_icon = "info"; break; } } if (timeout <= 0) { switch (m_type) { case Type::Warning: m_timeout = 7000; break; case Type::Error: m_timeout = 10000; break; default: m_timeout = 5000; break; } } } bool Toast::closed() const { return m_closed; } QString Toast::title() const { return m_title; } QString Toast::message() const { return m_message; } QString Toast::icon() const { return m_icon; } int Toast::timeout() const { return m_timeout; } Toast::Type Toast::type() const { return m_type; } void Toast::close() { if (!m_closed) { m_closed = true; emit closedChanged(); } if (m_locks.isEmpty()) { emit finishedClose(); } } void Toast::lock(QObject* sender) { m_locks << sender; QObject::connect(sender, &QObject::destroyed, this, &Toast::unlock); } void Toast::unlock(QObject* sender) { if (m_locks.remove(sender) && m_closed) { close(); } } Toaster::Toaster(QObject* parent) : QObject(parent) {} QQmlListProperty Toaster::toasts() { return QQmlListProperty(this, &m_toasts); } void Toaster::toast(const QString& title, const QString& message, const QString& icon, Toast::Type type, int timeout) { auto* toast = new Toast(title, message, icon, type, timeout, this); QObject::connect(toast, &Toast::finishedClose, this, [toast, this]() { if (m_toasts.removeOne(toast)) { emit toastsChanged(); toast->deleteLater(); } }); m_toasts.push_front(toast); emit toastsChanged(); } } // namespace caelestia ================================================ FILE: plugin/src/Caelestia/toaster.hpp ================================================ #pragma once #include #include #include #include namespace caelestia { class Toast : public QObject { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("Toast instances can only be retrieved from a Toaster") Q_PROPERTY(bool closed READ closed NOTIFY closedChanged) Q_PROPERTY(QString title READ title CONSTANT) Q_PROPERTY(QString message READ message CONSTANT) Q_PROPERTY(QString icon READ icon CONSTANT) Q_PROPERTY(int timeout READ timeout CONSTANT) Q_PROPERTY(Type type READ type CONSTANT) public: enum class Type { Info = 0, Success, Warning, Error }; Q_ENUM(Type) explicit Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout, QObject* parent = nullptr); [[nodiscard]] bool closed() const; [[nodiscard]] QString title() const; [[nodiscard]] QString message() const; [[nodiscard]] QString icon() const; [[nodiscard]] int timeout() const; [[nodiscard]] Type type() const; Q_INVOKABLE void close(); Q_INVOKABLE void lock(QObject* sender); Q_INVOKABLE void unlock(QObject* sender); signals: void closedChanged(); void finishedClose(); private: QSet m_locks; bool m_closed; QString m_title; QString m_message; QString m_icon; Type m_type; int m_timeout; }; class Toaster : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON Q_PROPERTY(QQmlListProperty toasts READ toasts NOTIFY toastsChanged) public: explicit Toaster(QObject* parent = nullptr); [[nodiscard]] QQmlListProperty toasts(); Q_INVOKABLE void toast(const QString& title, const QString& message, const QString& icon = QString(), caelestia::Toast::Type type = Toast::Type::Info, int timeout = 5000); signals: void toastsChanged(); private: QList m_toasts; }; } // namespace caelestia ================================================ FILE: scripts/qml-lint-conventions.py ================================================ #!/usr/bin/env python3 """Checks QML files for Qt coding convention violations. https://doc.qt.io/qt-6/qml-codingconventions.html Required ordering within each QML object (with blank line between sections): 1. id 2. property declarations 3. signal declarations 4. JavaScript functions 5. object properties (bindings) 6. child objects 7. component definitions """ import re import sys from enum import IntEnum from pathlib import Path RED = "\033[0;31m" YELLOW = "\033[0;33m" CYAN = "\033[0;36m" GREEN = "\033[0;32m" MAGENTA = "\033[0;35m" BOLD = "\033[1m" RESET = "\033[0m" REPO_ROOT = Path(__file__).resolve().parent.parent class Section(IntEnum): ID = 0 PROPERTY = 1 SIGNAL = 2 FUNCTION = 3 BINDING = 4 CHILD = 5 COMPONENT_DEF = 6 SECTION_NAMES = { Section.ID: "id", Section.PROPERTY: "property declarations", Section.SIGNAL: "signal declarations", Section.FUNCTION: "functions", Section.BINDING: "bindings", Section.CHILD: "child objects", Section.COMPONENT_DEF: "component definitions", } RULE_COLOURS = { "section-order": YELLOW, "missing-section-separator": CYAN, "blank-after-open-brace": MAGENTA, "blank-before-close-brace": MAGENTA, } # Regexes PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s") SIGNAL_RE = re.compile(r"^signal\s") FUNCTION_RE = re.compile(r"^function\s") ID_RE = re.compile(r"^id\s*:\s*[a-zA-Z_]\w*\s*$") ENUM_RE = re.compile(r"^enum\s") COMPONENT_DEF_RE = re.compile(r"^component\s+\w+\s*:") COMMENT_LINE_RE = re.compile(r"^//") BLOCK_COMMENT_START = re.compile(r"/\*") BLOCK_COMMENT_END = re.compile(r"\*/") BINDING_RE = re.compile(r"^[a-z][a-zA-Z0-9_.]*\s*:") SIGNAL_HANDLER_RE = re.compile(r"^on[A-Z][a-zA-Z]*\s*:") # Child object: starts with uppercase or is a known child-like pattern CHILD_OBJECT_RE = re.compile(r"^[A-Z][a-zA-Z0-9_.]*\s*\{") # Inline component: Component { ... } INLINE_COMPONENT_RE = re.compile(r"^Component\s*\{") # Behavior on {, NumberAnimation on {, etc. BEHAVIOR_ON_RE = re.compile(r"^[A-Z]\w+\s+on\s+\w[\w.]*\s*\{") # Attached signal handler: Component.onCompleted:, Drag.onDragStarted:, etc. ATTACHED_HANDLER_RE = re.compile(r"^[A-Z]\w+\.on[A-Z]\w*\s*:") class Violation: def __init__(self, file: str, line: int, rule: str, msg: str): self.file = file self.line = line self.rule = rule self.msg = msg def __str__(self): c = RULE_COLOURS.get(self.rule, "") return f"{c}[{self.rule}]{RESET} {self.file}:{self.line}: {self.msg}" class ScopeTracker: """Tracks the current section and last-seen state for one indent level.""" def __init__(self): self.last_section: Section | None = None self.last_section_line: int = 0 self.had_blank_before_current: bool = True # no separator needed at start def get_indent(line: str) -> str: return line[: len(line) - len(line.lstrip())] def classify_line(stripped: str) -> Section | None: """Classify a stripped QML line into a section category.""" if ID_RE.match(stripped): return Section.ID if PROPERTY_DECL_RE.match(stripped): return Section.PROPERTY if SIGNAL_RE.match(stripped): return Section.SIGNAL if FUNCTION_RE.match(stripped): return Section.FUNCTION if ENUM_RE.match(stripped): return Section.PROPERTY # enums go with declarations if COMPONENT_DEF_RE.match(stripped): return Section.COMPONENT_DEF if BEHAVIOR_ON_RE.match(stripped): return Section.CHILD if CHILD_OBJECT_RE.match(stripped): return Section.CHILD if INLINE_COMPONENT_RE.match(stripped): return Section.CHILD if BINDING_RE.match(stripped) or SIGNAL_HANDLER_RE.match(stripped): return Section.BINDING if ATTACHED_HANDLER_RE.match(stripped): return Section.BINDING return None def check_file(filepath: Path) -> list[Violation]: violations = [] rel = str(filepath.relative_to(REPO_ROOT)) try: lines = filepath.read_text().splitlines() except (OSError, UnicodeDecodeError): return violations scopes: dict[str, ScopeTracker] = {} # indent -> tracker in_block_comment = False func_skip_depth = 0 # brace depth for skipping function bodies only prev_blank: dict[str, bool] = {} # indent -> was previous relevant line a blank? for i, line in enumerate(lines): lineno = i + 1 stripped = line.strip() indent = get_indent(line) # Handle block comments if in_block_comment: if BLOCK_COMMENT_END.search(stripped): in_block_comment = False continue if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): in_block_comment = True continue # Track blank lines per indent if not stripped: # Check: blank line right after opening brace of a QML object if i > 0 and func_skip_depth == 0 and not in_block_comment and lines[i - 1].strip().endswith("{"): violations.append( Violation( rel, lineno, "blank-after-open-brace", "no blank line expected after opening brace", ) ) for key in prev_blank: prev_blank[key] = True continue # Skip line comments if COMMENT_LINE_RE.match(stripped): continue # Skip inside function bodies (JS code, not QML structure) if func_skip_depth > 0: func_skip_depth += stripped.count("{") - stripped.count("}") if func_skip_depth <= 0: func_skip_depth = 0 continue # Closing brace: pop all scopes deeper than this indent # (the scope at this indent belongs to the parent object and must persist) if stripped == "}": # Check: blank line right before closing brace if i > 0 and not lines[i - 1].strip(): violations.append( Violation( rel, lineno, "blank-before-close-brace", "no blank line expected before closing brace", ) ) to_remove = [k for k in scopes if len(k) > len(indent)] for k in to_remove: del scopes[k] prev_blank.pop(k, None) continue section = classify_line(stripped) if section is None: continue # Get or create scope tracker for this indent if indent not in scopes: scopes[indent] = ScopeTracker() prev_blank[indent] = True # treat start of object as having separator tracker = scopes[indent] had_blank = prev_blank.get(indent, True) # --- Check 1: Section ordering --- if tracker.last_section is not None and section < tracker.last_section: violations.append( Violation( rel, lineno, "section-order", f"{SECTION_NAMES[section]} should appear before " f"{SECTION_NAMES[tracker.last_section]} " f"(seen at line {tracker.last_section_line})", ) ) # --- Check 2: Missing blank line between different sections --- if tracker.last_section is not None and section != tracker.last_section and not had_blank: violations.append( Violation( rel, lineno, "missing-section-separator", f"blank line expected between {SECTION_NAMES[tracker.last_section]} and {SECTION_NAMES[section]}", ) ) # Update tracker if tracker.last_section is None or section >= tracker.last_section: tracker.last_section = section tracker.last_section_line = lineno prev_blank[indent] = False # Skip function bodies (they contain JS, not QML structure) brace_count = stripped.count("{") - stripped.count("}") if brace_count > 0 and section == Section.FUNCTION: func_skip_depth = brace_count # Skip JS blocks in bindings (signal handlers, attached handlers, # and expression blocks like `color: { ... }`) if brace_count > 0 and section == Section.BINDING: colon_idx = stripped.index(":") after_colon = stripped[colon_idx + 1 :].strip() # If content after : doesn't start with an uppercase type name, # it's a JS block (not an inline QML object like `contentItem: Rect {`) if not re.match(r"^[A-Z]", after_colon): func_skip_depth = brace_count # Child object/component opening resets deeper scopes if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): to_remove = [k for k in scopes if len(k) > len(indent)] for k in to_remove: del scopes[k] prev_blank.pop(k, None) return violations def main(): qml_files = sorted(p for p in REPO_ROOT.rglob("*.qml") if "build" not in p.parts) print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n") all_violations: list[Violation] = [] for f in qml_files: all_violations.extend(check_file(f)) for v in all_violations: print(v) print() if all_violations: by_rule: dict[str, int] = {} for v in all_violations: by_rule[v.rule] = by_rule.get(v.rule, 0) + 1 for rule, count in sorted(by_rule.items()): print(f" {RULE_COLOURS.get(rule, '')}{rule}{RESET}: {count}") print(f"\n{BOLD}Found {len(all_violations)} violation(s).{RESET}") return 1 else: print(f"{BOLD}No violations found.{RESET}") return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: services/Audio.qml ================================================ pragma Singleton import qs.config import Caelestia.Services import Caelestia import Quickshell import Quickshell.Services.Pipewire import QtQuick Singleton { id: root property string previousSinkName: "" property string previousSourceName: "" property list sinks: [] property list sources: [] property list streams: [] readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode source: Pipewire.defaultAudioSource readonly property bool muted: !!sink?.audio?.muted readonly property real volume: sink?.audio?.volume ?? 0 readonly property bool sourceMuted: !!source?.audio?.muted readonly property real sourceVolume: source?.audio?.volume ?? 0 readonly property alias cava: cava readonly property alias beatTracker: beatTracker function setVolume(newVolume: real): void { if (sink?.ready && sink?.audio) { sink.audio.muted = false; sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); } } function incrementVolume(amount: real): void { setVolume(volume + (amount || Config.services.audioIncrement)); } function decrementVolume(amount: real): void { setVolume(volume - (amount || Config.services.audioIncrement)); } function setSourceVolume(newVolume: real): void { if (source?.ready && source?.audio) { source.audio.muted = false; source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); } } function incrementSourceVolume(amount: real): void { setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); } function decrementSourceVolume(amount: real): void { setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); } function setAudioSink(newSink: PwNode): void { Pipewire.preferredDefaultAudioSink = newSink; } function setAudioSource(newSource: PwNode): void { Pipewire.preferredDefaultAudioSource = newSource; } function setStreamVolume(stream: PwNode, newVolume: real): void { if (stream?.ready && stream?.audio) { stream.audio.muted = false; stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); } } function setStreamMuted(stream: PwNode, muted: bool): void { if (stream?.ready && stream?.audio) { stream.audio.muted = muted; } } function getStreamVolume(stream: PwNode): real { return stream?.audio?.volume ?? 0; } function getStreamMuted(stream: PwNode): bool { return !!stream?.audio?.muted; } function getStreamName(stream: PwNode): string { if (!stream) return qsTr("Unknown"); // Try application name first, then description, then name return stream.properties["application.name"] || stream.description || stream.name || qsTr("Unknown Application"); } onSinkChanged: { if (!sink?.ready) return; const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); previousSinkName = newSinkName; } onSourceChanged: { if (!source?.ready) return; const newSourceName = source.description || source.name || qsTr("Unknown Device"); if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); previousSourceName = newSourceName; } Component.onCompleted: { previousSinkName = sink?.description || sink?.name || qsTr("Unknown Device"); previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); } Connections { function onValuesChanged(): void { const newSinks = []; const newSources = []; const newStreams = []; for (const node of Pipewire.nodes.values) { if (!node.isStream) { if (node.isSink) newSinks.push(node); else if (node.audio) newSources.push(node); } else if (node.audio) { newStreams.push(node); } } root.sinks = newSinks; root.sources = newSources; root.streams = newStreams; } target: Pipewire.nodes } PwObjectTracker { objects: [...root.sinks, ...root.sources, ...root.streams] } CavaProvider { id: cava bars: Config.services.visualiserBars } BeatTracker { id: beatTracker } } ================================================ FILE: services/Brightness.qml ================================================ pragma Singleton pragma ComponentBehavior: Bound import qs.config import qs.components.misc import Quickshell import Quickshell.Io import QtQuick Singleton { id: root property list ddcMonitors: [] readonly property var ddcMonitorMap: { const map = {}; for (const m of ddcMonitors) map[m.connector] = m; return map; } readonly property list monitors: variants.instances // qmllint disable incompatible-type property bool appleDisplayPresent: false function getMonitorForScreen(screen: ShellScreen): var { return monitors.find(m => m.modelData === screen); // qmllint disable missing-property } function getMonitor(query: string): var { if (query === "active") { return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property } if (query.startsWith("model:")) { const model = query.slice(6); return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property } if (query.startsWith("serial:")) { const serial = query.slice(7); return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property } if (query.startsWith("id:")) { const id = parseInt(query.slice(3), 10); return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property } return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property } function increaseBrightness(): void { const monitor = getMonitor("active"); if (monitor) monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); } function decreaseBrightness(): void { const monitor = getMonitor("active"); if (monitor) monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); } onMonitorsChanged: { ddcMonitors = []; ddcProc.running = true; } Variants { id: variants model: Quickshell.screens // Don't respect excluded screens cause ipc Monitor {} } Process { running: true command: ["sh", "-c", "asdbctl get"] // To avoid warnings if asdbctl is not installed stdout: StdioCollector { onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 } } Process { id: ddcProc command: ["ddcutil", "detect", "--brief"] stdout: StdioCollector { onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({ busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1], connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-" })) } } CustomShortcut { name: "brightnessUp" description: "Increase brightness" onPressed: root.increaseBrightness() } CustomShortcut { name: "brightnessDown" description: "Decrease brightness" onPressed: root.decreaseBrightness() } IpcHandler { function get(): real { return getFor("active"); } // Allows searching by active/model/serial/id/name function getFor(query: string): real { return root.getMonitor(query)?.brightness ?? -1; } function set(value: string): string { return setFor("active", value); } // Handles brightness value like brightnessctl: 0.1, +0.1, 0.1-, 10%, +10%, 10%- function setFor(query: string, value: string): string { const monitor = root.getMonitor(query); if (!monitor) return "Invalid monitor: " + query; let targetBrightness; if (value.endsWith("%-")) { const percent = parseFloat(value.slice(0, -2)); targetBrightness = monitor.brightness - (percent / 100); } else if (value.startsWith("+") && value.endsWith("%")) { const percent = parseFloat(value.slice(1, -1)); targetBrightness = monitor.brightness + (percent / 100); } else if (value.endsWith("%")) { const percent = parseFloat(value.slice(0, -1)); targetBrightness = percent / 100; } else if (value.startsWith("+")) { const increment = parseFloat(value.slice(1)); targetBrightness = monitor.brightness + increment; } else if (value.endsWith("-")) { const decrement = parseFloat(value.slice(0, -1)); targetBrightness = monitor.brightness - decrement; } else if (value.includes("%") || value.includes("-") || value.includes("+")) { return `Invalid brightness format: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; } else { targetBrightness = parseFloat(value); } if (isNaN(targetBrightness)) return `Failed to parse value: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; monitor.setBrightness(targetBrightness); return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; } target: "brightness" } component Monitor: QtObject { id: monitor required property ShellScreen modelData readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null readonly property bool isDdc: ddcInfo !== null readonly property string busNum: ddcInfo?.busNum ?? "" readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") property real brightness property real queuedBrightness: NaN readonly property Process initProc: Process { stdout: StdioCollector { onStreamFinished: { if (monitor.isAppleDisplay) { const val = parseInt(text.trim()); monitor.brightness = val / 101; } else { const [, , , cur, max] = text.split(" "); monitor.brightness = parseInt(cur) / parseInt(max); } } } } readonly property Timer timer: Timer { interval: 500 onTriggered: { if (!isNaN(monitor.queuedBrightness)) { monitor.setBrightness(monitor.queuedBrightness); monitor.queuedBrightness = NaN; } } } function setBrightness(value: real): void { value = Math.max(0, Math.min(1, value)); const rounded = Math.round(value * 100); if (Math.round(brightness * 100) === rounded) return; if (isDdc && timer.running) { queuedBrightness = value; return; } brightness = value; if (isAppleDisplay) Quickshell.execDetached(["asdbctl", "set", rounded]); else if (isDdc) Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]); else Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]); if (isDdc) timer.restart(); } function initBrightness(): void { if (isAppleDisplay) initProc.command = ["asdbctl", "get"]; else if (isDdc) initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]; else initProc.command = ["sh", "-c", "echo a b c $(brightnessctl g) $(brightnessctl m)"]; initProc.running = true; } onBusNumChanged: initBrightness() Component.onCompleted: initBrightness() } } ================================================ FILE: services/Colours.qml ================================================ pragma Singleton pragma ComponentBehavior: Bound import qs.services import qs.config import qs.utils import Caelestia import Quickshell import Quickshell.Io import QtQuick Singleton { id: root property bool showPreview property string scheme property string flavour readonly property bool light: showPreview ? previewLight : currentLight property bool currentLight property bool previewLight readonly property M3Palette palette: showPreview ? preview : current readonly property M3TPalette tPalette: M3TPalette {} readonly property M3Palette current: M3Palette {} readonly property M3Palette preview: M3Palette {} readonly property Transparency transparency: Transparency {} readonly property alias wallLuminance: analyser.luminance function getLuminance(c: color): real { if (c.r == 0 && c.g == 0 && c.b == 0) return 0; return Math.sqrt(0.299 * (c.r ** 2) + 0.587 * (c.g ** 2) + 0.114 * (c.b ** 2)); } function alterColour(c: color, a: real, layer: int): color { const luminance = getLuminance(c); 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)); const scale = (luminance + offset) / luminance; const r = Math.max(0, Math.min(1, c.r * scale)); const g = Math.max(0, Math.min(1, c.g * scale)); const b = Math.max(0, Math.min(1, c.b * scale)); return Qt.rgba(r, g, b, a); } function layer(c: color, layer: var): color { if (!transparency.enabled) return c; return layer === 0 ? Qt.alpha(c, transparency.base) : alterColour(c, transparency.layers, layer ?? 1); } function on(c: color): color { if (c.hslLightness < 0.5) return Qt.hsla(c.hslHue, c.hslSaturation, 0.9, 1); return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1); } function load(data: string, isPreview: bool): void { const colours = isPreview ? preview : current; const scheme = JSON.parse(data); if (!isPreview) { root.scheme = scheme.name; flavour = scheme.flavour; currentLight = scheme.mode === "light"; } else { previewLight = scheme.mode === "light"; } for (const [name, colour] of Object.entries(scheme.colours)) { const propName = name.startsWith("term") ? name : `m3${name}`; if (colours.hasOwnProperty(propName)) colours[propName] = `#${colour}`; } } function setMode(mode: string): void { Quickshell.execDetached(["caelestia", "scheme", "set", "--notify", "-m", mode]); } FileView { path: `${Paths.state}/scheme.json` watchChanges: true onFileChanged: reload() onLoaded: root.load(text(), false) } ImageAnalyser { id: analyser source: Wallpapers.current } component Transparency: QtObject { readonly property bool enabled: Appearance.transparency.enabled readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0) readonly property real layers: Appearance.transparency.layers } component M3TPalette: QtObject { readonly property color m3primary_paletteKeyColor: root.layer(root.palette.m3primary_paletteKeyColor) readonly property color m3secondary_paletteKeyColor: root.layer(root.palette.m3secondary_paletteKeyColor) readonly property color m3tertiary_paletteKeyColor: root.layer(root.palette.m3tertiary_paletteKeyColor) readonly property color m3neutral_paletteKeyColor: root.layer(root.palette.m3neutral_paletteKeyColor) readonly property color m3neutral_variant_paletteKeyColor: root.layer(root.palette.m3neutral_variant_paletteKeyColor) readonly property color m3background: root.layer(root.palette.m3background, 0) readonly property color m3onBackground: root.layer(root.palette.m3onBackground) readonly property color m3surface: root.layer(root.palette.m3surface, 0) readonly property color m3surfaceDim: root.layer(root.palette.m3surfaceDim, 0) readonly property color m3surfaceBright: root.layer(root.palette.m3surfaceBright, 0) readonly property color m3surfaceContainerLowest: root.layer(root.palette.m3surfaceContainerLowest) readonly property color m3surfaceContainerLow: root.layer(root.palette.m3surfaceContainerLow) readonly property color m3surfaceContainer: root.layer(root.palette.m3surfaceContainer) readonly property color m3surfaceContainerHigh: root.layer(root.palette.m3surfaceContainerHigh) readonly property color m3surfaceContainerHighest: root.layer(root.palette.m3surfaceContainerHighest) readonly property color m3onSurface: root.layer(root.palette.m3onSurface) readonly property color m3surfaceVariant: root.layer(root.palette.m3surfaceVariant, 0) readonly property color m3onSurfaceVariant: root.layer(root.palette.m3onSurfaceVariant) readonly property color m3inverseSurface: root.layer(root.palette.m3inverseSurface, 0) readonly property color m3inverseOnSurface: root.layer(root.palette.m3inverseOnSurface) readonly property color m3outline: root.layer(root.palette.m3outline) readonly property color m3outlineVariant: root.layer(root.palette.m3outlineVariant) readonly property color m3shadow: root.layer(root.palette.m3shadow) readonly property color m3scrim: root.layer(root.palette.m3scrim) readonly property color m3surfaceTint: root.layer(root.palette.m3surfaceTint) readonly property color m3primary: root.layer(root.palette.m3primary) readonly property color m3onPrimary: root.layer(root.palette.m3onPrimary) readonly property color m3primaryContainer: root.layer(root.palette.m3primaryContainer) readonly property color m3onPrimaryContainer: root.layer(root.palette.m3onPrimaryContainer) readonly property color m3inversePrimary: root.layer(root.palette.m3inversePrimary) readonly property color m3secondary: root.layer(root.palette.m3secondary) readonly property color m3onSecondary: root.layer(root.palette.m3onSecondary) readonly property color m3secondaryContainer: root.layer(root.palette.m3secondaryContainer) readonly property color m3onSecondaryContainer: root.layer(root.palette.m3onSecondaryContainer) readonly property color m3tertiary: root.layer(root.palette.m3tertiary) readonly property color m3onTertiary: root.layer(root.palette.m3onTertiary) readonly property color m3tertiaryContainer: root.layer(root.palette.m3tertiaryContainer) readonly property color m3onTertiaryContainer: root.layer(root.palette.m3onTertiaryContainer) readonly property color m3error: root.layer(root.palette.m3error) readonly property color m3onError: root.layer(root.palette.m3onError) readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer) readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer) readonly property color m3success: root.layer(root.palette.m3success) readonly property color m3onSuccess: root.layer(root.palette.m3onSuccess) readonly property color m3successContainer: root.layer(root.palette.m3successContainer) readonly property color m3onSuccessContainer: root.layer(root.palette.m3onSuccessContainer) readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed) readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim) readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed) readonly property color m3onPrimaryFixedVariant: root.layer(root.palette.m3onPrimaryFixedVariant) readonly property color m3secondaryFixed: root.layer(root.palette.m3secondaryFixed) readonly property color m3secondaryFixedDim: root.layer(root.palette.m3secondaryFixedDim) readonly property color m3onSecondaryFixed: root.layer(root.palette.m3onSecondaryFixed) readonly property color m3onSecondaryFixedVariant: root.layer(root.palette.m3onSecondaryFixedVariant) readonly property color m3tertiaryFixed: root.layer(root.palette.m3tertiaryFixed) readonly property color m3tertiaryFixedDim: root.layer(root.palette.m3tertiaryFixedDim) readonly property color m3onTertiaryFixed: root.layer(root.palette.m3onTertiaryFixed) readonly property color m3onTertiaryFixedVariant: root.layer(root.palette.m3onTertiaryFixedVariant) } component M3Palette: QtObject { property color m3primary_paletteKeyColor: "#a8627b" property color m3secondary_paletteKeyColor: "#8e6f78" property color m3tertiary_paletteKeyColor: "#986e4c" property color m3neutral_paletteKeyColor: "#807477" property color m3neutral_variant_paletteKeyColor: "#837377" property color m3background: "#191114" property color m3onBackground: "#efdfe2" property color m3surface: "#191114" property color m3surfaceDim: "#191114" property color m3surfaceBright: "#403739" property color m3surfaceContainerLowest: "#130c0e" property color m3surfaceContainerLow: "#22191c" property color m3surfaceContainer: "#261d20" property color m3surfaceContainerHigh: "#31282a" property color m3surfaceContainerHighest: "#3c3235" property color m3onSurface: "#efdfe2" property color m3surfaceVariant: "#514347" property color m3onSurfaceVariant: "#d5c2c6" property color m3inverseSurface: "#efdfe2" property color m3inverseOnSurface: "#372e30" property color m3outline: "#9e8c91" property color m3outlineVariant: "#514347" property color m3shadow: "#000000" property color m3scrim: "#000000" property color m3surfaceTint: "#ffb0ca" property color m3primary: "#ffb0ca" property color m3onPrimary: "#541d34" property color m3primaryContainer: "#6f334a" property color m3onPrimaryContainer: "#ffd9e3" property color m3inversePrimary: "#8b4a62" property color m3secondary: "#e2bdc7" property color m3onSecondary: "#422932" property color m3secondaryContainer: "#5a3f48" property color m3onSecondaryContainer: "#ffd9e3" property color m3tertiary: "#f0bc95" property color m3onTertiary: "#48290c" property color m3tertiaryContainer: "#b58763" property color m3onTertiaryContainer: "#000000" property color m3error: "#ffb4ab" property color m3onError: "#690005" property color m3errorContainer: "#93000a" property color m3onErrorContainer: "#ffdad6" property color m3success: "#B5CCBA" property color m3onSuccess: "#213528" property color m3successContainer: "#374B3E" property color m3onSuccessContainer: "#D1E9D6" property color m3primaryFixed: "#ffd9e3" property color m3primaryFixedDim: "#ffb0ca" property color m3onPrimaryFixed: "#39071f" property color m3onPrimaryFixedVariant: "#6f334a" property color m3secondaryFixed: "#ffd9e3" property color m3secondaryFixedDim: "#e2bdc7" property color m3onSecondaryFixed: "#2b151d" property color m3onSecondaryFixedVariant: "#5a3f48" property color m3tertiaryFixed: "#ffdcc3" property color m3tertiaryFixedDim: "#f0bc95" property color m3onTertiaryFixed: "#2f1500" property color m3onTertiaryFixedVariant: "#623f21" property color term0: "#353434" property color term1: "#ff4c8a" property color term2: "#ffbbb7" property color term3: "#ffdedf" property color term4: "#b3a2d5" property color term5: "#e98fb0" property color term6: "#ffba93" property color term7: "#eed1d2" property color term8: "#b39e9e" property color term9: "#ff80a3" property color term10: "#ffd3d0" property color term11: "#fff1f0" property color term12: "#dcbc93" property color term13: "#f9a8c2" property color term14: "#ffd1c0" property color term15: "#ffffff" } } ================================================ FILE: services/GameMode.qml ================================================ pragma Singleton import qs.services import qs.config import Caelestia import Quickshell import Quickshell.Io import QtQuick Singleton { id: root property alias enabled: props.enabled function setDynamicConfs(): void { Hypr.extras.applyOptions({ "animations:enabled": 0, "decoration:shadow:enabled": 0, "decoration:blur:enabled": 0, "general:gaps_in": 0, "general:gaps_out": 0, "general:border_size": 1, "decoration:rounding": 0, "general:allow_tearing": 1 }); } onEnabledChanged: { if (enabled) { setDynamicConfs(); if (Config.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad"); } else { Hypr.extras.message("reload"); if (Config.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad"); } } PersistentProperties { id: props property bool enabled: Hypr.options["animations:enabled"] === 0 // qmllint disable missing-property reloadableId: "gameMode" } Connections { function onConfigReloaded(): void { if (props.enabled) root.setDynamicConfs(); } target: Hypr } IpcHandler { function isEnabled(): bool { return props.enabled; } function toggle(): void { props.enabled = !props.enabled; } function enable(): void { props.enabled = true; } function disable(): void { props.enabled = false; } target: "gameMode" } } ================================================ FILE: services/Hypr.qml ================================================ pragma Singleton import qs.components.misc import qs.config import Caelestia import Caelestia.Internal import Quickshell import Quickshell.Hyprland import Quickshell.Io import QtQuick Singleton { id: root readonly property var toplevels: Hyprland.toplevels readonly property var workspaces: Hyprland.workspaces readonly property var monitors: Hyprland.monitors readonly property HyprlandToplevel activeToplevel: { const t = Hyprland.activeToplevel; return t?.workspace?.name.startsWith("special:") || Hyprland.focusedWorkspace?.toplevels.values.length > 0 ? t : null; } readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor readonly property int activeWsId: focusedWorkspace?.id ?? 1 readonly property HyprKeyboard keyboard: extras.devices.keyboards.find(kb => kb.main) ?? null readonly property bool capsLock: keyboard?.capsLock ?? false readonly property bool numLock: keyboard?.numLock ?? false readonly property string defaultKbLayout: keyboard?.layout.split(",")[0] ?? "??" readonly property string kbLayoutFull: keyboard?.activeKeymap ?? "Unknown" readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??" readonly property var kbMap: new Map() readonly property alias extras: extras readonly property alias options: extras.options readonly property alias devices: extras.devices property bool hadKeyboard property string lastSpecialWorkspace: "" signal configReloaded function dispatch(request: string): void { Hyprland.dispatch(request); } function cycleSpecialWorkspace(direction: string): void { const openSpecials = workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0); if (openSpecials.length === 0) return; const activeSpecial = focusedMonitor.lastIpcObject.specialWorkspace.name ?? ""; if (!activeSpecial) { if (lastSpecialWorkspace) { const workspace = workspaces.values.find(w => w.name === lastSpecialWorkspace); if (workspace && workspace.lastIpcObject.windows > 0) { dispatch(`workspace ${lastSpecialWorkspace}`); return; } } dispatch(`workspace ${openSpecials[0].name}`); return; } const currentIndex = openSpecials.findIndex(w => w.name === activeSpecial); let nextIndex = 0; if (currentIndex !== -1) { if (direction === "next") nextIndex = (currentIndex + 1) % openSpecials.length; else nextIndex = (currentIndex - 1 + openSpecials.length) % openSpecials.length; } dispatch(`workspace ${openSpecials[nextIndex].name}`); } function monitorNames(): list { return monitors.values.map(e => e.name); } function monitorFor(screen: ShellScreen): HyprlandMonitor { return Hyprland.monitorFor(screen); } function reloadDynamicConfs(): void { extras.batchMessage(["keyword bindlni ,Caps_Lock,global,caelestia:refreshDevices", "keyword bindlni ,Num_Lock,global,caelestia:refreshDevices"]); } Component.onCompleted: reloadDynamicConfs() onCapsLockChanged: { if (!Config.utilities.toasts.capsLockChanged) return; if (capsLock) Toaster.toast(qsTr("Caps lock enabled"), qsTr("Caps lock is currently enabled"), "keyboard_capslock_badge"); else Toaster.toast(qsTr("Caps lock disabled"), qsTr("Caps lock is currently disabled"), "keyboard_capslock"); } onNumLockChanged: { if (!Config.utilities.toasts.numLockChanged) return; if (numLock) Toaster.toast(qsTr("Num lock enabled"), qsTr("Num lock is currently enabled"), "looks_one"); else Toaster.toast(qsTr("Num lock disabled"), qsTr("Num lock is currently disabled"), "timer_1"); } onKbLayoutFullChanged: { if (hadKeyboard && Config.utilities.toasts.kbLayoutChanged) Toaster.toast(qsTr("Keyboard layout changed"), qsTr("Layout changed to: %1").arg(kbLayoutFull), "keyboard"); hadKeyboard = !!keyboard; } Connections { function onRawEvent(event: HyprlandEvent): void { const n = event.name; if (n.endsWith("v2")) return; if (n === "configreloaded") { root.configReloaded(); root.reloadDynamicConfs(); } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { Hyprland.refreshWorkspaces(); Hyprland.refreshMonitors(); } else if (["openwindow", "closewindow", "movewindow"].includes(n)) { Hyprland.refreshToplevels(); Hyprland.refreshWorkspaces(); } else if (n.includes("mon")) { Hyprland.refreshMonitors(); } else if (n.includes("workspace")) { Hyprland.refreshWorkspaces(); } else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) { Hyprland.refreshToplevels(); } } target: Hyprland } Connections { function onLastIpcObjectChanged(): void { const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; if (specialName && specialName.startsWith("special:")) { root.lastSpecialWorkspace = specialName; } } target: root.focusedMonitor } FileView { id: kbLayoutFile path: Quickshell.env("CAELESTIA_XKB_RULES_PATH") || "/usr/share/X11/xkb/rules/base.lst" onLoaded: { const layoutMatch = text().match(/! layout\n([\s\S]*?)\n\n/); if (layoutMatch) { const lines = layoutMatch[1].split("\n"); for (const line of lines) { if (!line.trim() || line.trim().startsWith("!")) continue; const match = line.match(/^\s*([a-z]{2,})\s+([a-zA-Z() ]+)$/); if (match) root.kbMap.set(match[2], match[1]); } } const variantMatch = text().match(/! variant\n([\s\S]*?)\n\n/); if (variantMatch) { const lines = variantMatch[1].split("\n"); for (const line of lines) { if (!line.trim() || line.trim().startsWith("!")) continue; const match = line.match(/^\s*([a-zA-Z0-9_-]+)\s+([a-z]{2,}): (.+)$/); if (match) root.kbMap.set(match[3], match[2]); } } } } IpcHandler { function refreshDevices(): void { extras.refreshDevices(); } function cycleSpecialWorkspace(direction: string): void { root.cycleSpecialWorkspace(direction); } function listSpecialWorkspaces(): string { return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); } target: "hypr" } CustomShortcut { name: "refreshDevices" description: "Reload devices" onPressed: extras.refreshDevices() onReleased: extras.refreshDevices() } HyprExtras { id: extras } } ================================================ FILE: services/IdleInhibitor.qml ================================================ pragma Singleton import Quickshell import Quickshell.Io import Quickshell.Wayland Singleton { id: root property alias enabled: props.enabled readonly property alias enabledSince: props.enabledSince onEnabledChanged: { if (enabled) props.enabledSince = new Date(); } PersistentProperties { id: props property bool enabled property date enabledSince reloadableId: "idleInhibitor" } IdleInhibitor { enabled: props.enabled window: PanelWindow { implicitWidth: 0 implicitHeight: 0 color: "transparent" mask: Region {} } } IpcHandler { function isEnabled(): bool { return props.enabled; } function toggle(): void { props.enabled = !props.enabled; } function enable(): void { props.enabled = true; } function disable(): void { props.enabled = false; } target: "idleInhibitor" } } ================================================ FILE: services/LyricsService.qml ================================================ pragma Singleton import qs.config import qs.utils import Caelestia import QtQuick import Quickshell import Quickshell.Io import "../utils/scripts/lrcparser.js" as Lrc Singleton { id: root property var player: Players.active property int currentIndex: -1 property bool loading: false property bool isManualSeeking: false property bool lyricsVisible: Config.services.showLyrics property string backend: "Local" property real currentSongId: 0 property real offset readonly property string lyricsDir: Paths.absolutePath(Config.paths.lyricsDir) readonly property string lyricsMapFile: Paths.absolutePath(Config.paths.lyricsDir) + "/lyrics_map.json" property int currentRequestId: 0 // The data source for the UI readonly property alias model: lyricsModel readonly property alias candidatesModel: fetchedCandidatesModel property var lyricsMap: ({}) // shared headers for all NetEase requests readonly property var _netEaseHeaders: ({ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", "Referer": "https://music.163.com/" }) ListModel { id: lyricsModel } ListModel { id: fetchedCandidatesModel } Timer { id: seekTimer interval: 500 onTriggered: root.isManualSeeking = false } // If no local lyrics were loaded within the interval, fall back to NetEase Timer { id: fallbackTimer interval: 200 onTriggered: { if (lyricsModel.count === 0) { root.backend = "NetEase"; fallbackToOnline(); } } } Timer { id: loadDebounce interval: 50 onTriggered: root._doLoadLyrics() } FileView { id: lyricsMapFileView path: root.lyricsMapFile printErrors: false onLoaded: { try { root.lyricsMap = JSON.parse(text()); } catch (e) { root.lyricsMap = {}; } } } FileView { id: lrcFile printErrors: false onLoaded: { fallbackTimer.stop(); let parsed = Lrc.parseLrc(text()); if (parsed.length > 0) { root.backend = "Local"; updateModel(parsed); loading = false; } else { root.backend = "NetEase"; fallbackToOnline(); } } } Connections { function onActiveChanged() { root.player = Players.active; loadLyrics(); } target: Players } Connections { function onMetadataChanged() { loadLyrics(); } target: root.player ignoreUnknownSignals: true } Process { id: saveLyricsMap command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] } function getMetadata() { if (!player || !player.metadata) return null; let artist = player.metadata["xesam:artist"]; const title = player.metadata["xesam:title"]; if (Array.isArray(artist)) artist = artist.join(", "); return { artist: artist || "Unknown", title: title || "Unknown" }; } function _metaKey(meta) { return `${meta.artist} - ${meta.title}`; } function savePrefs() { let meta = getMetadata(); if (!meta) return; let key = _metaKey(meta); let existing = root.lyricsMap[key] ?? {}; root.lyricsMap[key] = { offset: root.offset, backend: root.backend, neteaseId: existing.neteaseId ?? null }; // reassign to notify QML bindings of the map change root.lyricsMap = root.lyricsMap; saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; saveLyricsMap.running = true; } function toggleVisibility() { Config.services.showLyrics = !Config.services.showLyrics; Config.save(); } function loadLyrics() { loadDebounce.restart(); } function _doLoadLyrics() { const meta = getMetadata(); if (!meta) return; loading = true; lyricsModel.clear(); currentIndex = -1; root.currentSongId = 0; root.backend = "Local"; root.currentRequestId++; let requestId = root.currentRequestId; let key = _metaKey(meta); let saved = root.lyricsMap[key]; root.offset = saved?.offset ?? 0.0; if (saved?.neteaseId && saved?.backend === "NetEase") { root.backend = "NetEase"; root.currentSongId = saved.neteaseId; fetchNetEaseLyrics(saved.neteaseId, requestId); fetchNetEaseCandidates(meta.title, meta.artist, requestId); return; } if (saved?.backend === "NetEase") { fallbackTimer.restart(); return; } let cleanDir = lyricsDir.replace(/\/$/, ""); let fullPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; lrcFile.path = ""; lrcFile.path = fullPath; fetchNetEaseCandidates(meta.title, meta.artist, requestId); //to populate the list regardless // 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. if (saved?.backend !== "Local") fallbackTimer.restart(); } function updateModel(parsedArray) { root.currentIndex = -1; lyricsModel.clear(); for (let line of parsedArray) { lyricsModel.append({ time: line.time, lyricLine: line.text }); } } function fallbackToOnline() { let meta = getMetadata(); if (!meta) return; fetchNetEase(meta.title, meta.artist, root.currentRequestId); } // NetEase // searches NetEase and populates the candidates model. returns the result array via the onResults callback function _searchNetEase(title, artist, reqId, onResults) { Requests.resetCookies(); const query = encodeURIComponent(`${title} ${artist}`); const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; Requests.get(url, text => { if (reqId !== root.currentRequestId) return; const res = JSON.parse(text); const songs = res.result?.songs || []; fetchedCandidatesModel.clear(); for (let s of songs) { fetchedCandidatesModel.append({ id: s.id, title: s.name || "Unknown Title", artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" }); } onResults(songs); }, err => {}, root._netEaseHeaders); } // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. function fetchNetEaseCandidates(title, artist, reqId) { _searchNetEase(title, artist, reqId, _songs => {}); } // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. function fetchNetEase(title, artist, reqId) { _searchNetEase(title, artist, reqId, songs => { const bestMatch = songs.find(s => { const inputArtist = String(artist || "").toLowerCase(); const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); }); if (!bestMatch) { return; // No reliable lyrics found } let key = `${artist} - ${title}`; root.lyricsMap[key] = { offset: root.lyricsMap[key]?.offset ?? 0.0, backend: "NetEase", neteaseId: bestMatch.id }; root.currentSongId = bestMatch.id; savePrefs(); fetchNetEaseLyrics(bestMatch.id, reqId); }); } function fetchNetEaseLyrics(id, reqId) { const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; Requests.get(url, text => { if (reqId !== root.currentRequestId) return; const res = JSON.parse(text); if (res.lrc?.lyric) { updateModel(Lrc.parseLrc(res.lrc.lyric)); loading = false; } }); } function selectCandidate(songId) { let meta = getMetadata(); if (!meta) return; root.backend = "NetEase"; root.currentSongId = songId; let key = _metaKey(meta); root.lyricsMap[key] = { offset: root.lyricsMap[key]?.offset ?? 0.0, neteaseId: songId }; savePrefs(); fetchNetEaseLyrics(songId, currentRequestId); } function updatePosition() { if (isManualSeeking || loading || !player || lyricsModel.count === 0) return; let pos = player.position - root.offset; let newIdx = -1; for (let i = lyricsModel.count - 1; i >= 0; i--) { if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor newIdx = i; break; } } if (newIdx !== currentIndex) { root.currentIndex = newIdx; } } function jumpTo(index, time) { root.isManualSeeking = true; root.currentIndex = index; if (player) { player.position = time + root.offset + 0.01; // compensate for rounding } seekTimer.restart(); } } ================================================ FILE: services/Network.qml ================================================ pragma Singleton import Quickshell import Quickshell.Io import QtQuick import qs.services Singleton { id: root readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true readonly property bool scanning: Nmcli.scanning property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property int ethernetDeviceCount: 0 property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null property var pendingConnection: null property list savedConnections: [] property list savedConnectionSsids: [] signal connectionFailed(string ssid) function enableWifi(enabled: bool): void { Nmcli.enableWifi(enabled, result => { if (result.success) { root.getWifiStatus(); Nmcli.getNetworks(() => { syncNetworksFromNmcli(); }); } }); } function toggleWifi(): void { Nmcli.toggleWifi(result => { if (result.success) { root.getWifiStatus(); Nmcli.getNetworks(() => { syncNetworksFromNmcli(); }); } }); } function rescanWifi(): void { Nmcli.rescanWifi(); } function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { // Set up pending connection tracking if callback provided if (callback) { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; } Nmcli.connectToNetwork(ssid, password, bssid, result => { if (result && result.success) { // Connection successful if (callback) callback(result); root.pendingConnection = null; } else if (result && result.needsPassword) { // Password needed - callback will handle showing dialog if (callback) callback(result); } else { // Connection failed if (result && result.error) { root.connectionFailed(ssid); } if (callback) callback(result); root.pendingConnection = null; } }); } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { // Set up pending connection tracking const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback }; Nmcli.connectToNetworkWithPasswordCheck(ssid, isSecure, result => { if (result && result.success) { // Connection successful if (callback) callback(result); root.pendingConnection = null; } else if (result && result.needsPassword) { // Password needed - callback will handle showing dialog if (callback) callback(result); } else { // Connection failed if (result && result.error) { root.connectionFailed(ssid); } if (callback) callback(result); root.pendingConnection = null; } }, bssid); } function disconnectFromNetwork(): void { // Try to disconnect - use connection name if available, otherwise use device Nmcli.disconnectFromNetwork(); // Refresh network list after disconnection Qt.callLater(() => { Nmcli.getNetworks(() => { syncNetworksFromNmcli(); }); }, 500); } function forgetNetwork(ssid: string): void { // Delete the connection profile for this network // This will remove the saved password and connection settings Nmcli.forgetNetwork(ssid, result => { if (result.success) { // Refresh network list after deletion Qt.callLater(() => { Nmcli.getNetworks(() => { syncNetworksFromNmcli(); }); }, 500); } }); } function syncNetworksFromNmcli(): void { const rNetworks = root.networks; const nNetworks = Nmcli.networks; // Build a map of existing networks by key const existingMap = new Map(); for (const rn of rNetworks) { const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; existingMap.set(key, rn); } // Build a map of new networks by key const newMap = new Map(); for (const nn of nNetworks) { const key = `${nn.frequency}:${nn.ssid}:${nn.bssid}`; newMap.set(key, nn); } // Remove networks that no longer exist for (const [key, network] of existingMap) { if (!newMap.has(key)) { const index = rNetworks.indexOf(network); if (index >= 0) { rNetworks.splice(index, 1); network.destroy(); } } } // Add or update networks from Nmcli for (const [key, nNetwork] of newMap) { const existing = existingMap.get(key); if (existing) { // Update existing network's lastIpcObject existing.lastIpcObject = nNetwork.lastIpcObject; } else { // Create new AccessPoint from Nmcli's data rNetworks.push(apComp.createObject(root, { lastIpcObject: nNetwork.lastIpcObject })); } } } function hasSavedProfile(ssid: string): bool { // Use Nmcli's hasSavedProfile which has the same logic return Nmcli.hasSavedProfile(ssid); } function getWifiStatus(): void { Nmcli.getWifiStatus(enabled => { root.wifiEnabled = enabled; }); } function getEthernetDevices(): void { root.ethernetProcessRunning = true; Nmcli.getEthernetInterfaces(interfaces => { root.ethernetDevices = Nmcli.ethernetDevices; root.ethernetDeviceCount = Nmcli.ethernetDevices.length; root.ethernetProcessRunning = false; }); } function connectEthernet(connectionName: string, interfaceName: string): void { Nmcli.connectEthernet(connectionName, interfaceName, result => { if (result.success) { getEthernetDevices(); // Refresh device details after connection Qt.callLater(() => { const activeDevice = root.ethernetDevices.find(function (d) { return d.connected; }); if (activeDevice && activeDevice.interface) { updateEthernetDeviceDetails(activeDevice.interface); } }, 1000); } }); } function disconnectEthernet(connectionName: string): void { Nmcli.disconnectEthernet(connectionName, result => { if (result.success) { getEthernetDevices(); // Clear device details after disconnection Qt.callLater(() => { root.ethernetDeviceDetails = null; }); } }); } function updateEthernetDeviceDetails(interfaceName: string): void { Nmcli.getEthernetDeviceDetails(interfaceName, details => { root.ethernetDeviceDetails = details; }); } function updateWirelessDeviceDetails(): void { // Find the wireless interface by looking for wifi devices // Pass empty string to let Nmcli find the active interface automatically Nmcli.getWirelessDeviceDetails("", details => { root.wirelessDeviceDetails = details; }); } function cidrToSubnetMask(cidr: string): string { // Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0") const cidrNum = parseInt(cidr); if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { return ""; } const mask = (0xffffffff << (32 - cidrNum)) >>> 0; const octets = [(mask >>> 24) & 0xff, (mask >>> 16) & 0xff, (mask >>> 8) & 0xff, mask & 0xff]; return octets.join("."); } Component.onCompleted: { // Trigger ethernet device detection after initialization Qt.callLater(() => { getEthernetDevices(); }); // Load saved connections on startup Nmcli.loadSavedConnections(() => { root.savedConnections = Nmcli.savedConnections; root.savedConnectionSsids = Nmcli.savedConnectionSsids; }); // Get initial WiFi status Nmcli.getWifiStatus(enabled => { root.wifiEnabled = enabled; }); // Sync networks from Nmcli on startup Qt.callLater(() => { syncNetworksFromNmcli(); }, 100); } // Sync saved connections from Nmcli when they're updated Connections { function onSavedConnectionsChanged() { root.savedConnections = Nmcli.savedConnections; } function onSavedConnectionSsidsChanged() { root.savedConnectionSsids = Nmcli.savedConnectionSsids; } target: Nmcli } Timer { id: monitorDebounce interval: 200 onTriggered: { Nmcli.getNetworks(() => { syncNetworksFromNmcli(); }); getEthernetDevices(); } } Process { running: true command: ["nmcli", "m"] stdout: SplitParser { onRead: monitorDebounce.start() } } component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid readonly property string bssid: lastIpcObject.bssid readonly property int strength: lastIpcObject.strength readonly property int frequency: lastIpcObject.frequency readonly property bool active: lastIpcObject.active readonly property string security: lastIpcObject.security readonly property bool isSecure: security.length > 0 } Component { id: apComp AccessPoint {} } } ================================================ FILE: services/NetworkUsage.qml ================================================ pragma Singleton import qs.config import Quickshell import Quickshell.Io import Caelestia.Internal import QtQuick Singleton { id: root property int refCount: 0 // Current speeds in bytes per second readonly property real downloadSpeed: _downloadSpeed readonly property real uploadSpeed: _uploadSpeed // Total bytes transferred since tracking started readonly property real downloadTotal: _downloadTotal readonly property real uploadTotal: _uploadTotal // History buffers for sparkline readonly property CircularBuffer downloadBuffer: _downloadBuffer readonly property CircularBuffer uploadBuffer: _uploadBuffer readonly property int historyLength: 30 // Private properties property real _downloadSpeed: 0 property real _uploadSpeed: 0 property real _downloadTotal: 0 property real _uploadTotal: 0 // Previous readings for calculating speed property real _prevRxBytes: 0 property real _prevTxBytes: 0 property real _prevTimestamp: 0 // Initial readings for calculating totals property real _initialRxBytes: 0 property real _initialTxBytes: 0 property bool _initialized: false function formatBytes(bytes: real): var { // Handle negative or invalid values if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { return { value: 0, unit: "B/s" }; } if (bytes < 1024) { return { value: bytes, unit: "B/s" }; } else if (bytes < 1024 * 1024) { return { value: bytes / 1024, unit: "KB/s" }; } else if (bytes < 1024 * 1024 * 1024) { return { value: bytes / (1024 * 1024), unit: "MB/s" }; } else { return { value: bytes / (1024 * 1024 * 1024), unit: "GB/s" }; } } function formatBytesTotal(bytes: real): var { // Handle negative or invalid values if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { return { value: 0, unit: "B" }; } if (bytes < 1024) { return { value: bytes, unit: "B" }; } else if (bytes < 1024 * 1024) { return { value: bytes / 1024, unit: "KB" }; } else if (bytes < 1024 * 1024 * 1024) { return { value: bytes / (1024 * 1024), unit: "MB" }; } else { return { value: bytes / (1024 * 1024 * 1024), unit: "GB" }; } } function parseNetDev(content: string): var { const lines = content.split("\n"); let totalRx = 0; let totalTx = 0; for (let i = 2; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const parts = line.split(/\s+/); if (parts.length < 10) continue; const iface = parts[0].replace(":", ""); // Skip loopback interface if (iface === "lo") continue; const rxBytes = parseFloat(parts[1]) || 0; const txBytes = parseFloat(parts[9]) || 0; totalRx += rxBytes; totalTx += txBytes; } return { rx: totalRx, tx: totalTx }; } CircularBuffer { id: _downloadBuffer capacity: root.historyLength + 1 } CircularBuffer { id: _uploadBuffer capacity: root.historyLength + 1 } FileView { id: netDevFile path: "/proc/net/dev" } Timer { interval: Config.dashboard.resourceUpdateInterval running: root.refCount > 0 repeat: true triggeredOnStart: true onTriggered: { netDevFile.reload(); const content = netDevFile.text(); if (!content) return; const data = root.parseNetDev(content); const now = Date.now(); if (!root._initialized) { root._initialRxBytes = data.rx; root._initialTxBytes = data.tx; root._prevRxBytes = data.rx; root._prevTxBytes = data.tx; root._prevTimestamp = now; root._initialized = true; return; } const timeDelta = (now - root._prevTimestamp) / 1000; // seconds if (timeDelta > 0) { // Calculate byte deltas let rxDelta = data.rx - root._prevRxBytes; let txDelta = data.tx - root._prevTxBytes; // Handle counter overflow (when counters wrap around from max to 0) // This happens when counters exceed 32-bit or 64-bit limits if (rxDelta < 0) { // Counter wrapped around - assume 64-bit counter rxDelta += Math.pow(2, 64); } if (txDelta < 0) { txDelta += Math.pow(2, 64); } // Calculate speeds root._downloadSpeed = rxDelta / timeDelta; root._uploadSpeed = txDelta / timeDelta; if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) _downloadBuffer.push(root._downloadSpeed); if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) _uploadBuffer.push(root._uploadSpeed); } // Calculate totals with overflow handling let downTotal = data.rx - root._initialRxBytes; let upTotal = data.tx - root._initialTxBytes; // Handle counter overflow for totals if (downTotal < 0) { downTotal += Math.pow(2, 64); } if (upTotal < 0) { upTotal += Math.pow(2, 64); } root._downloadTotal = downTotal; root._uploadTotal = upTotal; root._prevRxBytes = data.rx; root._prevTxBytes = data.tx; root._prevTimestamp = now; } } } ================================================ FILE: services/Nmcli.qml ================================================ pragma Singleton pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io import QtQuick Singleton { id: root property var deviceStatus: null property var wirelessInterfaces: [] property var ethernetInterfaces: [] property bool isConnected: false property string activeInterface: "" property string activeConnection: "" property bool wifiEnabled: true readonly property bool scanning: rescanProc.running readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property list savedConnections: [] property list savedConnectionSsids: [] property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null property var wirelessDeviceDetails: null property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property list activeProcesses: [] readonly property alias connectionCheckTimer: connectionCheckTimer readonly property alias immediateCheckTimer: immediateCheckTimer // Constants readonly property string deviceTypeWifi: "wifi" readonly property string deviceTypeEthernet: "ethernet" readonly property string connectionTypeWireless: "802-11-wireless" readonly property string nmcliCommandDevice: "device" readonly property string nmcliCommandConnection: "connection" readonly property string nmcliCommandWifi: "wifi" readonly property string nmcliCommandRadio: "radio" readonly property string deviceStatusFields: "DEVICE,TYPE,STATE,CONNECTION" readonly property string connectionListFields: "NAME,TYPE" readonly property string wirelessSsidField: "802-11-wireless.ssid" readonly property string networkListFields: "SSID,SIGNAL,SECURITY" readonly property string networkDetailFields: "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY" readonly property string securityKeyMgmt: "802-11-wireless-security.key-mgmt" readonly property string securityPsk: "802-11-wireless-security.psk" readonly property string keyMgmtWpaPsk: "wpa-psk" readonly property string connectionParamType: "type" readonly property string connectionParamConName: "con-name" readonly property string connectionParamIfname: "ifname" readonly property string connectionParamSsid: "ssid" readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" signal connectionFailed(string ssid) function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; } 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"); } function parseNetworkOutput(output: string): list { if (!output || output.length === 0) { return []; } const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; const rep = new RegExp("\\\\:", "g"); const rep2 = new RegExp(PLACEHOLDER, "g"); const allNetworks = output.trim().split("\n").filter(line => line && line.length > 0).map(n => { const net = n.replace(rep, PLACEHOLDER).split(":"); return { active: net[0] === "yes", strength: parseInt(net[1] || "0", 10) || 0, frequency: parseInt(net[2] || "0", 10) || 0, ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), security: (net[5] ?? "").trim() }; }).filter(n => n.ssid && n.ssid.length > 0); return allNetworks; } function deduplicateNetworks(networks: list): list { if (!networks || networks.length === 0) { return []; } const networkMap = new Map(); for (const network of networks) { const existing = networkMap.get(network.ssid); if (!existing) { networkMap.set(network.ssid, network); } else { if (network.active && !existing.active) { networkMap.set(network.ssid, network); } else if (!network.active && !existing.active) { if (network.strength > existing.strength) { networkMap.set(network.ssid, network); } } } } return Array.from(networkMap.values()); } function isConnectionCommand(command: list): bool { if (!command || command.length === 0) { return false; } return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); } function parseDeviceStatusOutput(output: string, filterType: string): list { if (!output || output.length === 0) { return []; } const interfaces = []; const lines = output.trim().split("\n"); for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const deviceType = parts[1]; let shouldInclude = false; if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { shouldInclude = true; } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { shouldInclude = true; } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { shouldInclude = true; } if (shouldInclude) { interfaces.push({ device: parts[0] || "", type: parts[1] || "", state: parts[2] || "", connection: parts[3] || "" }); } } } return interfaces; } function isConnectedState(state: string): bool { if (!state || state.length === 0) { return false; } return state === "100 (connected)" || state === "connected" || state.startsWith("connected"); } function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.command = ["nmcli", ...args]; proc.callback = callback; activeProcesses.push(proc); proc.processFinished.connect(() => { const index = activeProcesses.indexOf(proc); if (index >= 0) { activeProcesses.splice(index, 1); } }); Qt.callLater(() => { proc.exec(proc.command); }); } function getDeviceStatus(callback: var): void { executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { if (callback) callback(result.output); }); } function getWirelessInterfaces(callback: var): void { executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); root.wirelessInterfaces = interfaces; if (callback) callback(interfaces); }); } function getEthernetInterfaces(callback: var): void { executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); const devices = []; for (const iface of interfaces) { const connected = isConnectedState(iface.state); devices.push({ interface: iface.device, type: iface.type, state: iface.state, connection: iface.connection, connected: connected, ipAddress: "", gateway: "", dns: [], subnet: "", macAddress: "", speed: "" }); } root.ethernetInterfaces = interfaces; root.ethernetDevices = devices; if (callback) callback(interfaces); }); } function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { if (connectionName && connectionName.length > 0) { executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); if (interfaceName && interfaceName.length > 0) { Qt.callLater(() => { getEthernetDeviceDetails(interfaceName, () => {}); }, 1000); } }, 500); } if (callback) callback(result); }); } else if (interfaceName && interfaceName.length > 0) { executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); Qt.callLater(() => { getEthernetDeviceDetails(interfaceName, () => {}); }, 1000); }, 500); } if (callback) callback(result); }); } else { if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); } } function disconnectEthernet(connectionName: string, callback: var): void { if (!connectionName || connectionName.length === 0) { if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); return; } executeCommand([root.nmcliCommandConnection, "down", connectionName], result => { if (result.success) { root.ethernetDeviceDetails = null; Qt.callLater(() => { getEthernetInterfaces(() => {}); }, 500); } if (callback) callback(result); }); } function getAllInterfaces(callback: var): void { executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, "both"); if (callback) callback(interfaces); }); } function isInterfaceConnected(interfaceName: string, callback: var): void { executeCommand([root.nmcliCommandDevice, "status"], result => { const lines = result.output.trim().split("\n"); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length >= 3 && parts[0] === interfaceName) { const connected = isConnectedState(parts[2]); if (callback) callback(connected); return; } } if (callback) callback(false); }); } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { if (isSecure) { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; connectWireless(ssid, "", bssid, result => { if (result.success) { if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); } else if (result.needsPassword) { if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); } else { if (callback) callback(result); } }); } else { connectWireless(ssid, "", bssid, callback); } } function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { connectWireless(ssid, password, bssid, callback); } function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; const retries = retryCount !== undefined ? retryCount : 0; const maxRetries = 2; if (callback) { root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; connectionCheckTimer.start(); immediateCheckTimer.checkCount = 0; immediateCheckTimer.start(); } if (password && password.length > 0 && hasBssid) { const bssidUpper = bssid.toUpperCase(); createConnectionWithPassword(ssid, bssidUpper, password, callback); return; } let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; if (password && password.length > 0) { cmd.push(root.connectionParamPassword, password); } executeCommand(cmd, result => { if (result.needsPassword && callback) { if (callback) callback(result); return; } if (!result.success && root.pendingConnection && retries < maxRetries) { console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); Qt.callLater(() => { connectWireless(ssid, password, bssid, callback, retries + 1); }, 1000); } else if (!result.success && root.pendingConnection) {} else if (result.success && callback) {} else if (!result.success && !root.pendingConnection) { if (callback) callback(result); } }); } function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { checkAndDeleteConnection(ssid, () => { 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]; executeCommand(cmd, result => { if (result.success) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { const hasDuplicateWarning = result.error && (result.error.includes("another connection with the name") || result.error.includes("Reference the connection by its uuid")); if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; executeCommand(fallbackCmd, fallbackResult => { if (callback) callback(fallbackResult); }); } } }); }); } function checkAndDeleteConnection(ssid: string, callback: var): void { executeCommand([root.nmcliCommandConnection, "show", ssid], result => { if (result.success) { executeCommand([root.nmcliCommandConnection, "delete", ssid], deleteResult => { Qt.callLater(() => { if (callback) callback(); }, 300); }); } else { if (callback) callback(); } }); } function activateConnection(connectionName: string, callback: var): void { executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { if (callback) callback(result); }); } function loadSavedConnections(callback: var): void { executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], result => { if (!result.success) { root.savedConnections = []; root.savedConnectionSsids = []; if (callback) callback([]); return; } parseConnectionList(result.output, callback); }); } function parseConnectionList(output: string, callback: var): void { const lines = output.trim().split("\n").filter(line => line.length > 0); const wifiConnections = []; const connections = []; for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const name = parts[0]; const type = parts[1]; connections.push(name); if (type === root.connectionTypeWireless) { wifiConnections.push(name); } } } root.savedConnections = connections; if (wifiConnections.length > 0) { root.wifiConnectionQueue = wifiConnections; root.currentSsidQueryIndex = 0; root.savedConnectionSsids = []; queryNextSsid(callback); } else { root.savedConnectionSsids = []; root.wifiConnectionQueue = []; if (callback) callback(root.savedConnectionSsids); } } function queryNextSsid(callback: var): void { if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; root.currentSsidQueryIndex++; executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], result => { if (result.success) { processSsidOutput(result.output); } queryNextSsid(callback); }); } else { root.wifiConnectionQueue = []; root.currentSsidQueryIndex = 0; if (callback) callback(root.savedConnectionSsids); } } function processSsidOutput(output: string): void { const lines = output.trim().split("\n"); for (const line of lines) { if (line.startsWith("802-11-wireless.ssid:")) { const ssid = line.substring("802-11-wireless.ssid:".length).trim(); if (ssid && ssid.length > 0) { const ssidLower = ssid.toLowerCase(); const exists = root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower); if (!exists) { const newList = root.savedConnectionSsids.slice(); newList.push(ssid); root.savedConnectionSsids = newList; } } } } } function hasSavedProfile(ssid: string): bool { if (!ssid || ssid.length === 0) { return false; } const ssidLower = ssid.toLowerCase().trim(); if (root.active && root.active.ssid) { const activeSsidLower = root.active.ssid.toLowerCase().trim(); if (activeSsidLower === ssidLower) { return true; } } const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower); if (hasSsid) { return true; } const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower); return hasConnectionName; } function forgetNetwork(ssid: string, callback: var): void { if (!ssid || ssid.length === 0) { if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); return; } const connectionName = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim()) || ssid; executeCommand([root.nmcliCommandConnection, "delete", connectionName], result => { if (result.success) { Qt.callLater(() => { loadSavedConnections(() => {}); }, 500); } if (callback) callback(result); }); } function disconnect(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { if (callback) callback(result.success ? result.output : ""); }); } else { executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { if (callback) callback(result.success ? result.output : ""); }); } } function disconnectFromNetwork(): void { if (active && active.ssid) { executeCommand([root.nmcliCommandConnection, "down", active.ssid], result => { if (result.success) { getNetworks(() => {}); } }); } else { executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { if (result.success) { getNetworks(() => {}); } }); } } function getDeviceDetails(interfaceName: string, callback: var): void { executeCommand([root.nmcliCommandDevice, "show", interfaceName], result => { if (callback) callback(result.output); }); } function refreshStatus(callback: var): void { getDeviceStatus(output => { const lines = output.trim().split("\n"); let connected = false; let activeIf = ""; let activeConn = ""; for (const line of lines) { const parts = line.split(":"); if (parts.length >= 4) { const state = parts[2] || ""; if (isConnectedState(state)) { connected = true; activeIf = parts[0] || ""; activeConn = parts[3] || ""; break; } } } root.isConnected = connected; root.activeInterface = activeIf; root.activeConnection = activeConn; if (callback) callback({ connected, interface: activeIf, connection: activeConn }); }); } function bringInterfaceUp(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (callback) { callback(result); } }); } else { if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); } } function bringInterfaceDown(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { if (callback) { callback(result); } }); } else { if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); } } function scanWirelessNetworks(interfaceName: string, callback: var): void { let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "rescan"]; if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, result => { if (callback) { callback(result); } }); } function rescanWifi(): void { rescanProc.running = true; } function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], result => { if (result.success) { getWifiStatus(status => { root.wifiEnabled = status; if (callback) callback(result); }); } else { if (callback) callback(result); } }); } function toggleWifi(callback: var): void { const newState = !root.wifiEnabled; enableWifi(newState, callback); } function getWifiStatus(callback: var): void { executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], result => { if (result.success) { const enabled = result.output.trim() === "enabled"; root.wifiEnabled = enabled; if (callback) callback(enabled); } else { if (callback) callback(root.wifiEnabled); } }); } function getNetworks(callback: var): void { executeCommand(["-g", root.networkDetailFields, "d", "w"], result => { if (!result.success) { if (callback) callback([]); return; } const allNetworks = parseNetworkOutput(result.output); const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; const newMap = new Map(); for (const n of networks) newMap.set(`${n.frequency}:${n.ssid}:${n.bssid}`, n); for (let i = rNetworks.length - 1; i >= 0; i--) { const rn = rNetworks[i]; const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; if (!newMap.has(key)) { rNetworks.splice(i, 1); rn.destroy(); } } const existingMap = new Map(); for (const rn of rNetworks) existingMap.set(`${rn.frequency}:${rn.ssid}:${rn.bssid}`, rn); for (const [key, network] of newMap) { const match = existingMap.get(key); if (match) { match.lastIpcObject = network; } else { rNetworks.push(apComp.createObject(root, { lastIpcObject: network })); } } if (callback) callback(root.networks); checkPendingConnection(); }); } function getWirelessSSIDs(interfaceName: string, callback: var): void { let cmd = ["-t", "-f", root.networkListFields, root.nmcliCommandDevice, root.nmcliCommandWifi, "list"]; if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, result => { if (!result.success) { if (callback) callback([]); return; } const ssids = []; const lines = result.output.trim().split("\n"); const seenSSIDs = new Set(); for (const line of lines) { if (!line || line.length === 0) continue; const parts = line.split(":"); if (parts.length >= 1) { const ssid = parts[0].trim(); if (ssid && ssid.length > 0 && !seenSSIDs.has(ssid)) { seenSSIDs.add(ssid); const signalStr = parts.length >= 2 ? parts[1].trim() : ""; const signal = signalStr ? parseInt(signalStr, 10) : 0; const security = parts.length >= 3 ? parts[2].trim() : ""; ssids.push({ ssid: ssid, signal: signalStr, signalValue: isNaN(signal) ? 0 : signal, security: security }); } } } ssids.sort((a, b) => { return b.signalValue - a.signalValue; }); if (callback) callback(ssids); }); } function handlePasswordRequired(proc: var, error: string, output: string, exitCode: int): bool { if (!proc || !error || error.length === 0) { return false; } if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { return false; } const needsPassword = detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; const pending = root.pendingConnection; root.pendingConnection = null; proc.callbackCalled = true; const result = { success: false, output: output || "", error: error, exitCode: exitCode, needsPassword: true }; if (pending.callback) { pending.callback(result); } if (proc.callback && proc.callback !== pending.callback) { proc.callback(result); } return true; } return false; } component CommandProcess: Process { id: proc property var callback: null property list command: [] property bool callbackCalled: false property int exitCode: 0 signal processFinished environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) stdout: StdioCollector { id: stdoutCollector } stderr: StdioCollector { id: stderrCollector onStreamFinished: { const error = text.trim(); if (error && error.length > 0) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; root.handlePasswordRequired(proc, error, output, -1); } } } onExited: code => { exitCode = code; Qt.callLater(() => { if (callbackCalled) { processFinished(); return; } if (proc.callback) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; const success = exitCode === 0; const cmdIsConnection = isConnectionCommand(proc.command); if (root.handlePasswordRequired(proc, error, output, exitCode)) { processFinished(); return; } const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); if (!success && cmdIsConnection && root.pendingConnection) { const failedSsid = root.pendingConnection.ssid; root.connectionFailed(failedSsid); } callbackCalled = true; callback({ success: success, output: output, error: error, exitCode: proc.exitCode, needsPassword: needsPassword || false }); processFinished(); } else { processFinished(); } }); } } Component { id: commandProc CommandProcess {} } component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid readonly property string bssid: lastIpcObject.bssid readonly property int strength: lastIpcObject.strength readonly property int frequency: lastIpcObject.frequency readonly property bool active: lastIpcObject.active readonly property string security: lastIpcObject.security readonly property bool isSecure: security.length > 0 } Component { id: apComp AccessPoint {} } Timer { id: connectionCheckTimer interval: 4000 onTriggered: { if (root.pendingConnection) { const connected = root.active && root.active.ssid === root.pendingConnection.ssid; if (!connected && root.pendingConnection.callback) { let foundPasswordError = false; for (let i = 0; i < root.activeProcesses.length; i++) { const proc = root.activeProcesses[i]; if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { if (root.isConnectionCommand(proc.command)) { const needsPassword = root.detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection) { const pending = root.pendingConnection; root.pendingConnection = null; immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; proc.callbackCalled = true; const result = { success: false, output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", error: error, exitCode: -1, needsPassword: true }; if (pending.callback) { pending.callback(result); } if (proc.callback && proc.callback !== pending.callback) { proc.callback(result); } foundPasswordError = true; break; } } } } } if (!foundPasswordError) { const pending = root.pendingConnection; const failedSsid = pending.ssid; root.pendingConnection = null; immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.connectionFailed(failedSsid); pending.callback({ success: false, output: "", error: "Connection timeout", exitCode: -1, needsPassword: false }); } } else if (connected) { root.pendingConnection = null; immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } } } } Timer { id: immediateCheckTimer property int checkCount: 0 interval: 500 repeat: true triggeredOnStart: false onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; if (connected) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); } root.pendingConnection = null; } else { for (let i = 0; i < root.activeProcesses.length; i++) { const proc = root.activeProcesses[i]; if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { if (root.isConnectionCommand(proc.command)) { const needsPassword = root.detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; const pending = root.pendingConnection; root.pendingConnection = null; proc.callbackCalled = true; const result = { success: false, output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", error: error, exitCode: -1, needsPassword: true }; if (pending.callback) { pending.callback(result); } if (proc.callback && proc.callback !== pending.callback) { proc.callback(result); } return; } } } } } if (checkCount >= 6) { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } } } else { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; } } } function checkPendingConnection(): void { if (root.pendingConnection) { Qt.callLater(() => { const connected = root.active && root.active.ssid === root.pendingConnection.ssid; if (connected) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); } root.pendingConnection = null; } else { if (!immediateCheckTimer.running) { immediateCheckTimer.start(); } } }); } } function cidrToSubnetMask(cidr: string): string { const cidrNum = parseInt(cidr, 10); if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { return ""; } const mask = (0xffffffff << (32 - cidrNum)) >>> 0; const octet1 = (mask >>> 24) & 0xff; const octet2 = (mask >>> 16) & 0xff; const octet3 = (mask >>> 8) & 0xff; const octet4 = mask & 0xff; return `${octet1}.${octet2}.${octet3}.${octet4}`; } function getWirelessDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.wirelessInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { if (callback) callback(null); return; } } executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.wirelessDeviceDetails = null; if (callback) callback(null); return; } const details = parseDeviceDetails(result.output, false); root.wirelessDeviceDetails = details; if (callback) callback(details); }); } function getEthernetDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { if (callback) callback(null); return; } } executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.ethernetDeviceDetails = null; if (callback) callback(null); return; } const details = parseDeviceDetails(result.output, true); root.ethernetDeviceDetails = details; if (callback) callback(details); }); } function parseDeviceDetails(output: string, isEthernet: bool): var { const details = { ipAddress: "", gateway: "", dns: [], subnet: "", macAddress: "", speed: "" }; if (!output || output.length === 0) { return details; } const lines = output.trim().split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const parts = line.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(":").trim(); if (key.startsWith("IP4.ADDRESS")) { const ipParts = value.split("/"); details.ipAddress = ipParts[0] || ""; if (ipParts[1]) { details.subnet = cidrToSubnetMask(ipParts[1]); } else { details.subnet = ""; } } else if (key === "IP4.GATEWAY") { if (value !== "--") { details.gateway = value; } } else if (key.startsWith("IP4.DNS")) { if (value !== "--" && value.length > 0) { details.dns.push(value); } } else if (isEthernet && key === "WIRED-PROPERTIES.MAC") { details.macAddress = value; } else if (isEthernet && key === "WIRED-PROPERTIES.SPEED") { details.speed = value; } else if (!isEthernet && key === "GENERAL.HWADDR") { details.macAddress = value; } } } return details; } Process { id: rescanProc command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] onExited: root.getNetworks() } Process { id: monitorProc running: true command: ["nmcli", "monitor"] environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) stdout: SplitParser { onRead: root.refreshOnConnectionChange() } onExited: monitorRestartTimer.start() } Timer { id: monitorRestartTimer interval: 2000 onTriggered: { monitorProc.running = true; } } function refreshOnConnectionChange(): void { getNetworks(networks => { const newActive = root.active; if (newActive && newActive.active) { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); } } if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); } } }, 500); } else { root.wirelessDeviceDetails = null; root.ethernetDeviceDetails = null; } getWirelessInterfaces(() => {}); getEthernetInterfaces(() => { if (root.activeEthernet && root.activeEthernet.connected) { Qt.callLater(() => { getEthernetDeviceDetails(root.activeEthernet.interface, () => {}); }, 500); } }); }); } Component.onCompleted: { getWifiStatus(() => {}); getNetworks(() => {}); loadSavedConnections(() => {}); getEthernetInterfaces(() => {}); Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); } } if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); } } }, 2000); } } ================================================ FILE: services/NotifData.qml ================================================ pragma ComponentBehavior: Bound import qs.services import qs.config import qs.utils import Caelestia import Quickshell import Quickshell.Services.Notifications import QtQuick QtObject { id: notif property bool popup property bool closed property var locks: new Set() property date time: new Date() property string timeStr: qsTr("now") readonly property Timer timeStrTimer: Timer { running: !notif.closed repeat: true interval: 5000 onTriggered: notif.updateTimeStr() } property Notification notification property string id property string summary property string body property string appIcon property string appName property string image property var hints // Hints are not persisted across restarts property real expireTimeout: Config.notifs.defaultExpireTimeout property int urgency: NotificationUrgency.Normal property bool resident property bool hasActionIcons property list actions readonly property Timer timer: Timer { running: true interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout onTriggered: { if (Config.notifs.expire) notif.popup = false; } } readonly property LazyLoader dummyImageLoader: LazyLoader { active: false PanelWindow { implicitWidth: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image color: "transparent" mask: Region {} Image { function tryCache(): void { if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) return; const cacheKey = notif.appName + notif.summary + notif.id; let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; for (let i = 0; i < cacheKey.length; i++) { ch = cacheKey.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); const cache = Paths.notifimagecache + "/${hash}.png"; CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { notif.image = cache; notif.dummyImageLoader.active = false; }); } anchors.fill: parent source: Qt.resolvedUrl(notif.image) fillMode: Image.PreserveAspectCrop cache: false asynchronous: true opacity: 0 onStatusChanged: tryCache() onWidthChanged: tryCache() onHeightChanged: tryCache() } } } readonly property Connections conn: Connections { function onClosed(): void { notif.close(); } function onSummaryChanged(): void { notif.summary = notif.notification.summary; } function onBodyChanged(): void { notif.body = notif.notification.body; } function onAppIconChanged(): void { notif.appIcon = notif.notification.appIcon; } function onAppNameChanged(): void { notif.appName = notif.notification.appName; } function onImageChanged(): void { notif.image = notif.notification.image; if (notif.notification?.image) notif.dummyImageLoader.active = true; } function onExpireTimeoutChanged(): void { notif.expireTimeout = notif.notification.expireTimeout; } function onUrgencyChanged(): void { notif.urgency = notif.notification.urgency; } function onResidentChanged(): void { notif.resident = notif.notification.resident; } function onHasActionIconsChanged(): void { notif.hasActionIcons = notif.notification.hasActionIcons; } function onActionsChanged(): void { // qmllint disable unresolved-type notif.actions = notif.notification.actions.map(a => ({ // qmllint enable unresolved-type identifier: a.identifier, text: a.text, invoke: () => a.invoke() })); } function onHintsChanged(): void { notif.hints = notif.notification.hints; } target: notif.notification } function updateTimeStr(): void { const diff = Date.now() - time.getTime(); const m = Math.floor(diff / 60000); if (m < 1) { timeStr = qsTr("now"); timeStrTimer.interval = 5000; } else { const h = Math.floor(m / 60); const d = Math.floor(h / 24); if (d > 0) { timeStr = `${d}d`; timeStrTimer.interval = 3600000; } else if (h > 0) { timeStr = `${h}h`; timeStrTimer.interval = 300000; } else { timeStr = `${m}m`; timeStrTimer.interval = m < 10 ? 30000 : 60000; } } } function lock(item: Item): void { locks.add(item); } function unlock(item: Item): void { locks.delete(item); if (closed) close(); } function close(): void { closed = true; if (locks.size === 0 && Notifs.list.includes(this)) { Notifs.list = Notifs.list.filter(n => n !== this); notification?.dismiss(); destroy(); } } Component.onCompleted: { if (!notification) return; id = notification.id; summary = notification.summary; body = notification.body; appIcon = notification.appIcon; appName = notification.appName; image = notification.image; if (notification?.image) dummyImageLoader.active = true; expireTimeout = notification.expireTimeout; hints = notification.hints; urgency = notification.urgency; resident = notification.resident; hasActionIcons = notification.hasActionIcons; actions = notification.actions.map(a => ({ identifier: a.identifier, text: a.text, invoke: () => a.invoke() })); } } ================================================ FILE: services/Notifs.qml ================================================ pragma Singleton pragma ComponentBehavior: Bound import qs.components.misc import qs.config import qs.services import qs.utils import Caelestia import Quickshell import Quickshell.Io import Quickshell.Services.Notifications import QtQuick Singleton { id: root property list list: [] readonly property list notClosed: list.filter(n => !n.closed) readonly property list popups: list.filter(n => n.popup) property alias dnd: props.dnd property bool loaded onDndChanged: { if (!Config.utilities.toasts.dndChanged) return; if (dnd) Toaster.toast(qsTr("Do not disturb enabled"), qsTr("Popup notifications are now disabled"), "do_not_disturb_on"); else Toaster.toast(qsTr("Do not disturb disabled"), qsTr("Popup notifications are now enabled"), "do_not_disturb_off"); } onListChanged: { if (loaded) saveTimer.restart(); } Timer { id: saveTimer interval: 1000 onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({ time: n.time, id: n.id, summary: n.summary, body: n.body, appIcon: n.appIcon, appName: n.appName, image: n.image, expireTimeout: n.expireTimeout, urgency: n.urgency, resident: n.resident, hasActionIcons: n.hasActionIcons, actions: n.actions })))) } PersistentProperties { id: props property bool dnd reloadableId: "notifs" } NotificationServer { id: server keepOnReload: false actionsSupported: true bodyHyperlinksSupported: true bodyImagesSupported: true bodyMarkupSupported: true imageSupported: true persistenceSupported: true onNotification: notif => { notif.tracked = true; const comp = notifComp.createObject(root, { popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), notification: notif }); root.list = [comp, ...root.list]; } } FileView { id: storage path: `${Paths.state}/notifs.json` onLoaded: { const data = JSON.parse(text()); for (const notif of data) root.list.push(notifComp.createObject(root, notif)); root.list.sort((a, b) => b.time - a.time); root.loaded = true; } onLoadFailed: err => { if (err === FileViewError.FileNotFound) { root.loaded = true; setText("[]"); } } } CustomShortcut { name: "clearNotifs" description: "Clear all notifications" onPressed: { for (const notif of root.list.slice()) notif.close(); } } IpcHandler { function clear(): void { for (const notif of root.list.slice()) notif.close(); } function isDndEnabled(): bool { return props.dnd; } function toggleDnd(): void { props.dnd = !props.dnd; } function enableDnd(): void { props.dnd = true; } function disableDnd(): void { props.dnd = false; } target: "notifs" } Component { id: notifComp NotifData {} } } ================================================ FILE: services/Players.qml ================================================ pragma Singleton import qs.components.misc import qs.config import Quickshell import Quickshell.Io import Quickshell.Services.Mpris import QtQml import Caelestia Singleton { id: root readonly property list list: Mpris.players.values readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null property alias manualActive: props.manualActive function getIdentity(player: MprisPlayer): string { const alias = Config.services.playerAliases.find(a => a.from === player.identity); return alias?.to ?? player.identity; } Connections { function onPostTrackChanged() { if (!Config.utilities.toasts.nowPlaying) { return; } if (root.active.trackArtist != "" && root.active.trackTitle != "") { Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note"); } } target: root.active } PersistentProperties { id: props property MprisPlayer manualActive reloadableId: "players" } CustomShortcut { name: "mediaToggle" description: "Toggle media playback" onPressed: { const active = root.active; if (active && active.canTogglePlaying) active.togglePlaying(); } } CustomShortcut { name: "mediaPrev" description: "Previous track" onPressed: { const active = root.active; if (active && active.canGoPrevious) active.previous(); } } CustomShortcut { name: "mediaNext" description: "Next track" onPressed: { const active = root.active; if (active && active.canGoNext) active.next(); } } CustomShortcut { name: "mediaStop" description: "Stop media playback" onPressed: root.active?.stop() } IpcHandler { function getActive(prop: string): string { const active = root.active; return active ? active[prop] ?? "Invalid property" : "No active player"; } function list(): string { return root.list.map(p => root.getIdentity(p)).join("\n"); } function play(): void { const active = root.active; if (active?.canPlay) active.play(); } function pause(): void { const active = root.active; if (active?.canPause) active.pause(); } function playPause(): void { const active = root.active; if (active?.canTogglePlaying) active.togglePlaying(); } function previous(): void { const active = root.active; if (active?.canGoPrevious) active.previous(); } function next(): void { const active = root.active; if (active?.canGoNext) active.next(); } function stop(): void { root.active?.stop(); } target: "mpris" } } ================================================ FILE: services/Recorder.qml ================================================ pragma Singleton import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property alias running: props.running readonly property alias paused: props.paused readonly property alias elapsed: props.elapsed property bool needsStart property list startArgs property bool needsStop property bool needsPause function start(extraArgs = []): void { needsStart = true; startArgs = extraArgs; checkProc.running = true; } function stop(): void { needsStop = true; checkProc.running = true; } function togglePause(): void { needsPause = true; checkProc.running = true; } PersistentProperties { id: props property bool running: false property bool paused: false property real elapsed: 0 // Might get too large for int reloadableId: "recorder" } Process { id: checkProc running: true command: ["pidof", "gpu-screen-recorder"] onExited: code => { props.running = code === 0; if (code === 0) { if (root.needsStop) { Quickshell.execDetached(["caelestia", "record"]); props.running = false; props.paused = false; } else if (root.needsPause) { Quickshell.execDetached(["caelestia", "record", "-p"]); props.paused = !props.paused; } } else if (root.needsStart) { Quickshell.execDetached(["caelestia", "record", ...root.startArgs]); props.running = true; props.paused = false; props.elapsed = 0; } root.needsStart = false; root.needsStop = false; root.needsPause = false; } } Connections { // enabled: props.running && !props.paused function onSecondsChanged(): void { props.elapsed++; } target: Time } } ================================================ FILE: services/Screens.qml ================================================ pragma Singleton import qs.config import qs.utils import Quickshell Singleton { id: root readonly property list screens: { const excluded = Config.general.excludedScreens; if (excluded.length === 0) return Quickshell.screens; return Quickshell.screens.filter(s => !Strings.testRegexList(excluded, s.name)); } function isExcluded(screen: ShellScreen): bool { return Strings.testRegexList(Config.general.excludedScreens, screen.name); } } ================================================ FILE: services/SystemUsage.qml ================================================ pragma Singleton import qs.config import Quickshell import Quickshell.Io import QtQuick Singleton { id: root // CPU properties property string cpuName: "" property real cpuPerc property real cpuTemp // GPU properties readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType property string autoGpuType: "NONE" property string gpuName: "" property real gpuPerc property real gpuTemp // Memory properties property real memUsed property real memTotal readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 // Storage properties (aggregated) readonly property real storagePerc: { let totalUsed = 0; let totalSize = 0; for (const disk of disks) { totalUsed += disk.used; totalSize += disk.total; } return totalSize > 0 ? totalUsed / totalSize : 0; } // Individual disks: Array of { mount, used, total, free, perc } property var disks: [] property real lastCpuIdle property real lastCpuTotal property int refCount function cleanCpuName(name: string): string { return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim(); } function cleanGpuName(name: string): string { return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim(); } function formatKib(kib: real): var { const mib = 1024; const gib = 1024 ** 2; const tib = 1024 ** 3; if (kib >= tib) return { value: kib / tib, unit: "TiB" }; if (kib >= gib) return { value: kib / gib, unit: "GiB" }; if (kib >= mib) return { value: kib / mib, unit: "MiB" }; return { value: kib, unit: "KiB" }; } Timer { running: root.refCount > 0 interval: Config.dashboard.resourceUpdateInterval repeat: true triggeredOnStart: true onTriggered: { stat.reload(); meminfo.reload(); storage.running = true; gpuUsage.running = true; sensors.running = true; } } // One-time CPU info detection (name) FileView { id: cpuinfoInit path: "/proc/cpuinfo" onLoaded: { const nameMatch = text().match(/model name\s*:\s*(.+)/); if (nameMatch) root.cpuName = root.cleanCpuName(nameMatch[1]); } } FileView { id: stat path: "/proc/stat" onLoaded: { const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); if (data) { const stats = data.slice(1).map(n => parseInt(n, 10)); const total = stats.reduce((a, b) => a + b, 0); const idle = stats[3] + (stats[4] ?? 0); const totalDiff = total - root.lastCpuTotal; const idleDiff = idle - root.lastCpuIdle; root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; root.lastCpuTotal = total; root.lastCpuIdle = idle; } } } FileView { id: meminfo path: "/proc/meminfo" onLoaded: { const data = text(); root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; } } Process { id: storage // Get physical disks with aggregated usage from their partitions // -J triggers JSON output. -b triggers bytes. command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] stdout: StdioCollector { onStreamFinished: { const data = JSON.parse(text); const diskList = []; const seenDevices = new Set(); // Helper to recursively sum usage from children (partitions, crypt, lvm) const aggregateUsage = dev => { let used = 0; let size = 0; let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/")); if (!seenDevices.has(dev.name)) { // lsblk returns null for empty/unformatted partitions, which parses to 0 here used = parseInt(dev.fsused) || 0; size = parseInt(dev.fssize) || 0; seenDevices.add(dev.name); } if (dev.children) { for (const child of dev.children) { const stats = aggregateUsage(child); used += stats.used; size += stats.size; if (stats.isRoot) isRoot = true; } } return { used, size, isRoot }; }; for (const dev of data.blockdevices) { // Only process physical disks at the top level if (dev.type === "disk" && !dev.name.startsWith("zram")) { const stats = aggregateUsage(dev); if (stats.size === 0) { continue; } const total = stats.size; const used = stats.used; diskList.push({ mount: dev.name, used: used / 1024 // KiB , total: total / 1024 // KiB , free: (total - used) / 1024, perc: total > 0 ? used / total : 0, hasRoot: stats.isRoot }); } } // Sort by putting the disk with root first, then sort the rest alphabetically root.disks = diskList.sort((a, b) => { if (a.hasRoot && !b.hasRoot) return -1; if (!a.hasRoot && b.hasRoot) return 1; return a.mount.localeCompare(b.mount); }); } } } // GPU name detection (one-time) Process { id: gpuNameDetect running: true 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"] stdout: StdioCollector { onStreamFinished: { const output = text.trim(); if (!output) return; // Check if it's from nvidia-smi (clean GPU name) if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { root.gpuName = root.cleanGpuName(output); } else if (output.toLowerCase().includes("rx")) { root.gpuName = root.cleanGpuName(output); } else { // Parse lspci output: extract name from brackets or after colon // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0) const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/); if (bracketMatch) { root.gpuName = root.cleanGpuName(bracketMatch[1]); } else { const colonMatch = output.match(/:\s*(.+)/); if (colonMatch) root.gpuName = root.cleanGpuName(colonMatch[1]); } } } } } Process { id: gpuTypeCheck running: !Config.services.gpuType 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"] stdout: StdioCollector { onStreamFinished: root.autoGpuType = text.trim() } } Process { id: gpuUsage 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"] stdout: StdioCollector { onStreamFinished: { if (root.gpuType === "GENERIC") { const percs = text.trim().split("\n"); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); root.gpuPerc = sum / percs.length / 100; } else if (root.gpuType === "NVIDIA") { const [usage, temp] = text.trim().split(","); root.gpuPerc = parseInt(usage, 10) / 100; root.gpuTemp = parseInt(temp, 10); } else { root.gpuPerc = 0; root.gpuTemp = 0; } } } } Process { id: sensors command: ["sensors"] environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) stdout: StdioCollector { onStreamFinished: { let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/); if (!cpuTemp) // If AMD Tdie pattern failed, try fallback on Tctl cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); if (cpuTemp) root.cpuTemp = parseFloat(cpuTemp[1]); if (root.gpuType !== "GENERIC") return; let eligible = false; let sum = 0; let count = 0; for (const line of text.trim().split("\n")) { if (line === "Adapter: PCI adapter") eligible = true; else if (line === "") eligible = false; else if (eligible) { let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); if (!match) // Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs) match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/); if (match) { sum += parseFloat(match[2]); count++; } } } root.gpuTemp = count > 0 ? sum / count : 0; } } } } ================================================ FILE: services/Time.qml ================================================ pragma Singleton import qs.config import Quickshell import QtQuick Singleton { property alias enabled: clock.enabled readonly property date date: clock.date readonly property int hours: clock.hours readonly property int minutes: clock.minutes readonly property int seconds: clock.seconds readonly property string timeStr: format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") readonly property list timeComponents: timeStr.split(":") readonly property string hourStr: timeComponents[0] ?? "" readonly property string minuteStr: timeComponents[1] ?? "" readonly property string amPmStr: timeComponents[2] ?? "" function format(fmt: string): string { return Qt.formatDateTime(clock.date, fmt); } SystemClock { id: clock precision: SystemClock.Seconds } } ================================================ FILE: services/VPN.qml ================================================ pragma Singleton import Quickshell import Quickshell.Io import QtQuick import qs.config import Caelestia Singleton { id: root property bool connected: false readonly property bool connecting: connectProc.running || disconnectProc.running readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) readonly property var providerInput: { const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); return enabledProvider || "wireguard"; } readonly property bool isCustomProvider: typeof providerInput === "object" readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput) readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : "" readonly property var currentConfig: { const name = providerName; const iface = interfaceName; const defaults = getBuiltinDefaults(name, iface); if (isCustomProvider) { const custom = providerInput; return { connectCmd: custom.connectCmd || defaults.connectCmd, disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd, interface: custom.interface || defaults.interface, displayName: custom.displayName || defaults.displayName }; } return defaults; } function getBuiltinDefaults(name, iface) { const builtins = { "wireguard": { connectCmd: ["pkexec", "wg-quick", "up", iface], disconnectCmd: ["pkexec", "wg-quick", "down", iface], interface: iface, displayName: iface }, "warp": { connectCmd: ["warp-cli", "connect"], disconnectCmd: ["warp-cli", "disconnect"], interface: "CloudflareWARP", displayName: "Warp" }, "netbird": { connectCmd: ["netbird", "up"], disconnectCmd: ["netbird", "down"], interface: "wt0", displayName: "NetBird" }, "tailscale": { connectCmd: ["tailscale", "up"], disconnectCmd: ["tailscale", "down"], interface: "tailscale0", displayName: "Tailscale" } }; return builtins[name] || { connectCmd: [name, "up"], disconnectCmd: [name, "down"], interface: iface || name, displayName: name }; } function connect(): void { if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { connectProc.exec(root.currentConfig.connectCmd); } } function disconnect(): void { if (connected && !connecting && root.currentConfig && root.currentConfig.disconnectCmd) { disconnectProc.exec(root.currentConfig.disconnectCmd); } } function toggle(): void { if (connected) { disconnect(); } else { connect(); } } function checkStatus(): void { if (root.enabled) { statusProc.running = true; } } onConnectedChanged: { if (!Config.utilities.toasts.vpnChanged) return; const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; if (connected) { Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); } else { Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); } } Component.onCompleted: root.enabled && statusCheckTimer.start() Process { id: nmMonitor running: root.enabled command: ["nmcli", "monitor"] stdout: SplitParser { onRead: statusCheckTimer.restart() } } Process { id: statusProc command: ["ip", "link", "show"] environment: ({ LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) stdout: StdioCollector { onStreamFinished: { const iface = root.currentConfig ? root.currentConfig.interface : ""; root.connected = iface && text.includes(iface + ":"); } } } Process { id: connectProc onExited: statusCheckTimer.start() stderr: StdioCollector { onStreamFinished: { const error = text.trim(); if (error && !error.includes("[#]") && !error.includes("already exists")) { console.warn("VPN connection error:", error); } else if (error.includes("already exists")) { root.connected = true; } } } } Process { id: disconnectProc onExited: statusCheckTimer.start() stderr: StdioCollector { onStreamFinished: { const error = text.trim(); if (error && !error.includes("[#]")) { console.warn("VPN disconnection error:", error); } } } } Timer { id: statusCheckTimer interval: 500 onTriggered: root.checkStatus() } } ================================================ FILE: services/Visibilities.qml ================================================ pragma Singleton import qs.components import qs.services import Quickshell Singleton { property var screens: new Map() property var bars: new Map() function load(screen: ShellScreen, visibilities: DrawerVisibilities): void { screens.set(Hypr.monitorFor(screen), visibilities); } function getForActive(): DrawerVisibilities { return screens.get(Hypr.focusedMonitor); } } ================================================ FILE: services/Wallpapers.qml ================================================ pragma Singleton import qs.services import qs.config import qs.utils import Caelestia.Models import Quickshell import Quickshell.Io import QtQuick Searcher { id: root readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt` readonly property list smartArg: Config.services.smartScheme ? [] : ["--no-smart"] property bool showPreview: false readonly property string current: showPreview ? previewPath : actualCurrent property string previewPath property string actualCurrent property bool previewColourLock function setWallpaper(path: string): void { actualCurrent = path; Quickshell.execDetached(["caelestia", "wallpaper", "-f", path, ...smartArg]); } function preview(path: string): void { previewPath = path; showPreview = true; if (Colours.scheme === "dynamic") getPreviewColoursProc.running = true; } function stopPreview(): void { showPreview = false; if (!previewColourLock) Colours.showPreview = false; } list: wallpapers.entries key: "relativePath" useFuzzy: Config.launcher.useFuzzy.wallpapers extraOpts: useFuzzy ? ({}) : ({ forward: false }) IpcHandler { function get(): string { return root.actualCurrent; } function set(path: string): void { root.setWallpaper(path); } function list(): string { return root.list.map(w => w.path).join("\n"); } target: "wallpaper" } FileView { path: root.currentNamePath watchChanges: true onFileChanged: reload() onLoaded: { root.actualCurrent = text().trim(); root.previewColourLock = false; } } FileSystemModel { id: wallpapers recursive: true path: Paths.wallsdir filter: FileSystemModel.Images } Process { id: getPreviewColoursProc command: ["caelestia", "wallpaper", "-p", root.previewPath, ...root.smartArg] stdout: StdioCollector { onStreamFinished: { Colours.load(text, true); Colours.showPreview = true; } } } } ================================================ FILE: services/Weather.qml ================================================ pragma Singleton import qs.config import qs.utils import Caelestia import Quickshell import QtQuick Singleton { id: root property string city property string loc property var cc property list forecast property list hourlyForecast readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string description: cc?.weatherDesc ?? qsTr("No weather") readonly property string temp: Config.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` readonly property int humidity: cc?.humidity ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0 readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property var cachedCities: new Map() function reload(): void { const configLocation = Config.services.weatherLocation; if (configLocation) { if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) { loc = configLocation; fetchCityFromCoords(configLocation); } else { fetchCoordsFromCity(configLocation); } } else if (!loc || timer.elapsed() > 900) { Requests.get("https://ipinfo.io/json", text => { const response = JSON.parse(text); if (response.loc) { loc = response.loc; city = response.city ?? ""; timer.restart(); } }); } } function fetchCityFromCoords(coords: string): void { if (cachedCities.has(coords)) { city = cachedCities.get(coords); return; } const [lat, lon] = coords.split(",").map(s => s.trim()); const fallbackToBigDataCloud = () => { const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`; Requests.get(fallbackUrl, text => { const geo = JSON.parse(text); const geoCity = geo.city || geo.locality; if (geoCity) { city = geoCity; cachedCities.set(coords, geoCity); } else { city = "Unknown City"; } }); }; const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; Requests.get(nominatimUrl, text => { const geo = JSON.parse(text).features?.[0]?.properties.geocoding; if (geo) { const geoCity = geo.type === "city" ? geo.name : geo.city; if (geoCity) { city = geoCity; cachedCities.set(coords, geoCity); return; } } fallbackToBigDataCloud(); }, fallbackToBigDataCloud); } function fetchCoordsFromCity(cityName: string): void { const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`; Requests.get(url, text => { const json = JSON.parse(text); if (json.results && json.results.length > 0) { const result = json.results[0]; loc = result.latitude + "," + result.longitude; city = result.name; } else { loc = ""; reload(); } }); } function fetchWeatherData(): void { const url = getWeatherUrl(); if (url === "") return; Requests.get(url, text => { const json = JSON.parse(text); if (!json.current || !json.daily) return; cc = { weatherCode: json.current.weather_code, weatherDesc: getWeatherCondition(json.current.weather_code), tempC: Math.round(json.current.temperature_2m), tempF: Math.round(toFahrenheit(json.current.temperature_2m)), feelsLikeC: Math.round(json.current.apparent_temperature), feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)), humidity: json.current.relative_humidity_2m, windSpeed: json.current.wind_speed_10m, isDay: json.current.is_day, sunrise: json.daily.sunrise[0], sunset: json.daily.sunset[0] }; const forecastList = []; for (let i = 0; i < json.daily.time.length; i++) forecastList.push({ date: json.daily.time[i], maxTempC: Math.round(json.daily.temperature_2m_max[i]), maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), minTempC: Math.round(json.daily.temperature_2m_min[i]), minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])), weatherCode: json.daily.weather_code[i], icon: Icons.getWeatherIcon(json.daily.weather_code[i]) }); forecast = forecastList; const hourlyList = []; const now = new Date(); for (let i = 0; i < json.hourly.time.length; i++) { const time = new Date(json.hourly.time[i]); if (time < now) continue; hourlyList.push({ timestamp: json.hourly.time[i], hour: time.getHours(), tempC: Math.round(json.hourly.temperature_2m[i]), tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])), weatherCode: json.hourly.weather_code[i], icon: Icons.getWeatherIcon(json.hourly.weather_code[i]) }); } hourlyForecast = hourlyList; }); } function toFahrenheit(celcius: real): real { return celcius * 9 / 5 + 32; } function getWeatherUrl(): string { if (!loc || loc.indexOf(",") === -1) return ""; const [lat, lon] = loc.split(",").map(s => s.trim()); const baseUrl = "https://api.open-meteo.com/v1/forecast"; 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"]; return baseUrl + "?" + params.join("&"); } function getWeatherCondition(code: string): string { const conditions = { "0": "Clear", "1": "Clear", "2": "Partly cloudy", "3": "Overcast", "45": "Fog", "48": "Fog", "51": "Drizzle", "53": "Drizzle", "55": "Drizzle", "56": "Freezing drizzle", "57": "Freezing drizzle", "61": "Light rain", "63": "Rain", "65": "Heavy rain", "66": "Light rain", "67": "Heavy rain", "71": "Light snow", "73": "Snow", "75": "Heavy snow", "77": "Snow", "80": "Light rain", "81": "Rain", "82": "Heavy rain", "85": "Light snow showers", "86": "Heavy snow showers", "95": "Thunderstorm", "96": "Thunderstorm with hail", "99": "Thunderstorm with hail" }; return conditions[code] || "Unknown"; } onLocChanged: fetchWeatherData() Connections { function onWeatherLocationChanged(): void { root.reload(); } target: Config.services } // Refresh current location hourly Timer { interval: 3600000 // 1 hour running: true repeat: true onTriggered: fetchWeatherData() } ElapsedTimer { id: timer } } ================================================ FILE: shell.qml ================================================ //@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QSG_RENDER_LOOP=threaded //@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 import "modules" import "modules/drawers" import "modules/background" import "modules/areapicker" import "modules/lock" import Quickshell ShellRoot { Background {} Drawers {} AreaPicker {} Lock { id: lock } Shortcuts {} BatteryMonitor {} IdleMonitors { lock: lock } } ================================================ FILE: utils/Icons.qml ================================================ pragma Singleton import qs.config import Quickshell import Quickshell.Services.Notifications import QtQuick Singleton { id: root readonly property var weatherIcons: ({ "0": "clear_day", "1": "clear_day", "2": "partly_cloudy_day", "3": "cloud", "45": "foggy", "48": "foggy", "51": "rainy", "53": "rainy", "55": "rainy", "56": "rainy", "57": "rainy", "61": "rainy", "63": "rainy", "65": "rainy", "66": "rainy", "67": "rainy", "71": "cloudy_snowing", "73": "cloudy_snowing", "75": "snowing_heavy", "77": "cloudy_snowing", "80": "rainy", "81": "rainy", "82": "rainy", "85": "cloudy_snowing", "86": "snowing_heavy", "95": "thunderstorm", "96": "thunderstorm", "99": "thunderstorm" }) readonly property var categoryIcons: ({ WebBrowser: "web", Printing: "print", Security: "security", Network: "chat", Archiving: "archive", Compression: "archive", Development: "code", IDE: "code", TextEditor: "edit_note", Audio: "music_note", Music: "music_note", Player: "music_note", Recorder: "mic", Game: "sports_esports", FileTools: "files", FileManager: "files", Filesystem: "files", FileTransfer: "files", Settings: "settings", DesktopSettings: "settings", HardwareSettings: "settings", TerminalEmulator: "terminal", ConsoleOnly: "terminal", Utility: "build", Monitor: "monitor_heart", Midi: "graphic_eq", Mixer: "graphic_eq", AudioVideoEditing: "video_settings", AudioVideo: "music_video", Video: "videocam", Building: "construction", Graphics: "photo_library", "2DGraphics": "photo_library", RasterGraphics: "photo_library", TV: "tv", System: "host", Office: "content_paste" }) // Checks if a name matches an icon config. Icon configs can have the following keys: // - name: The exact name of the icon // - regex: A regex to match against the name (takes priority over name) // - flags: The regex flags (only used if regex is set) // - icon: The icon to use function matchIconConfig(name: string, iconConfig: var): bool { if (!iconConfig.icon) return false; if (iconConfig.regex) { const re = new RegExp(iconConfig.regex, iconConfig.flags ?? ""); if (re.test(name)) return true; } else if (iconConfig.name === name) { return true; } return false; } function getAppIcon(name: string, fallback: string): string { const icon = DesktopEntries.heuristicLookup(name)?.icon; if (fallback !== "undefined") return Quickshell.iconPath(icon, fallback); return Quickshell.iconPath(icon); } function getAppCategoryIcon(name: string, fallback: string): string { for (const iconConfig of Config.bar.workspaces.windowIcons) if (matchIconConfig(name, iconConfig)) return iconConfig.icon; const categories = DesktopEntries.heuristicLookup(name)?.categories; if (categories) for (const [key, value] of Object.entries(categoryIcons)) if (categories.includes(key)) return value; return fallback; } function getNetworkIcon(strength: int, isSecure = false): string { if (isSecure) { if (strength >= 80) return "network_wifi_locked"; if (strength >= 60) return "network_wifi_3_bar_locked"; if (strength >= 40) return "network_wifi_2_bar_locked"; if (strength >= 20) return "network_wifi_1_bar_locked"; return "signal_wifi_0_bar"; } else { if (strength >= 80) return "network_wifi"; if (strength >= 60) return "network_wifi_3_bar"; if (strength >= 40) return "network_wifi_2_bar"; if (strength >= 20) return "network_wifi_1_bar"; return "signal_wifi_0_bar"; } } function getBluetoothIcon(icon: string): string { if (icon.includes("headset") || icon.includes("headphones")) return "headphones"; if (icon.includes("audio")) return "speaker"; if (icon.includes("phone")) return "smartphone"; if (icon.includes("mouse")) return "mouse"; if (icon.includes("keyboard")) return "keyboard"; return "bluetooth"; } function getWeatherIcon(code: string): string { if (weatherIcons.hasOwnProperty(code)) return weatherIcons[code]; return "air"; } function getNotifIcon(summary: string, urgency: int): string { summary = summary.toLowerCase(); if (summary.includes("reboot")) return "restart_alt"; if (summary.includes("recording")) return "screen_record"; if (summary.includes("battery")) return "power"; if (summary.includes("screenshot")) return "screenshot_monitor"; if (summary.includes("welcome")) return "waving_hand"; if (summary.includes("time") || summary.includes("a break")) return "schedule"; if (summary.includes("installed")) return "download"; if (summary.includes("update")) return "update"; if (summary.includes("unable to")) return "deployed_code_alert"; if (summary.includes("profile")) return "person"; if (summary.includes("file")) return "folder_copy"; if (urgency === NotificationUrgency.Critical) return "release_alert"; return "chat"; } function getVolumeIcon(volume: real, isMuted: bool): string { if (isMuted) return "no_sound"; if (volume >= 0.5) return "volume_up"; if (volume > 0) return "volume_down"; return "volume_mute"; } function getMicVolumeIcon(volume: real, isMuted: bool): string { if (!isMuted && volume > 0) return "mic"; return "mic_off"; } function getSpecialWsIcon(name: string): string { name = name.toLowerCase().slice("special:".length); for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) if (matchIconConfig(name, iconConfig)) return iconConfig.icon; if (name === "special") return "star"; if (name === "communication") return "forum"; if (name === "music") return "music_cast"; if (name === "todo") return "checklist"; if (name === "sysmon") return "monitor_heart"; return name[0].toUpperCase(); } function getTrayIcon(id: string, icon: string): string { for (const sub of Config.bar.tray.iconSubs) if (sub.id === id) return sub.image ? Qt.resolvedUrl(sub.image) : Quickshell.iconPath(sub.icon); if (icon.includes("?path=")) { const [name, path] = icon.split("?path="); icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`); } return icon; } } ================================================ FILE: utils/Images.qml ================================================ pragma Singleton import Quickshell Singleton { readonly property list validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"] readonly property list validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"] function isValidImageByName(name: string): bool { return validImageExtensions.some(t => name.endsWith(`.${t}`)); } } ================================================ FILE: utils/NetworkConnection.qml ================================================ pragma Singleton import qs.services import QtQuick /** * NetworkConnection * * Centralized utility for network connection logic. Provides a single source of truth * for connecting to wireless networks, eliminating code duplication across * controlcenter components and bar popouts. * * Usage: * ```qml * import qs.utils * * // With Session object (controlcenter) * NetworkConnection.handleConnect(network, session); * * // Without Session object (bar popouts) - provide password dialog callback * NetworkConnection.handleConnect(network, null, (network) => { * // Show password dialog * root.passwordNetwork = network; * root.showPasswordDialog = true; * }); * ``` */ QtObject { id: root /** * Handle network connection with automatic disconnection if needed. * If there's an active network different from the target, disconnects first, * then connects to the target network. * * @param network The network object to connect to (must have ssid property) * @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork) * @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts) */ function handleConnect(network, session, onPasswordNeeded): void { if (!network) { return; } if (Nmcli.active && Nmcli.active.ssid !== network.ssid) { Nmcli.disconnectFromNetwork(); Qt.callLater(() => { root.connectToNetwork(network, session, onPasswordNeeded); }); } else { root.connectToNetwork(network, session, onPasswordNeeded); } } /** * Connect to a wireless network. * Handles both secured and open networks, checks for saved profiles, * and shows password dialog if needed. * * @param network The network object to connect to (must have ssid, isSecure, bssid properties) * @param session Optional Session object (for controlcenter - must have network property with showPasswordDialog and pendingNetwork) * @param onPasswordNeeded Optional callback function(network) called when password is needed (for bar popouts) */ function connectToNetwork(network, session, onPasswordNeeded): void { if (!network) { return; } if (network.isSecure) { const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid); if (hasSavedProfile) { Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); } else { // Use password check with callback Nmcli.connectToNetworkWithPasswordCheck(network.ssid, network.isSecure, result => { if (result.needsPassword) { // Clear pending connection if exists if (Nmcli.pendingConnection) { Nmcli.connectionCheckTimer.stop(); Nmcli.immediateCheckTimer.stop(); Nmcli.immediateCheckTimer.checkCount = 0; Nmcli.pendingConnection = null; } // Handle password dialog - use session if available, otherwise use callback if (session && session.network) { session.network.showPasswordDialog = true; session.network.pendingNetwork = network; } else if (onPasswordNeeded) { onPasswordNeeded(network); } } }, network.bssid); } } else { Nmcli.connectToNetwork(network.ssid, "", network.bssid, null); } } /** * Connect to a wireless network with a provided password. * Used by password dialogs when the user has already entered a password. * * @param network The network object to connect to (must have ssid, bssid properties) * @param password The password to use for connection * @param onResult Optional callback function(result) called with connection result */ function connectWithPassword(network, password, onResult): void { if (!network) { return; } Nmcli.connectToNetwork(network.ssid, password || "", network.bssid || "", onResult || null); } } ================================================ FILE: utils/Paths.qml ================================================ pragma Singleton import qs.config import Caelestia import Quickshell import QtQuick Singleton { id: root readonly property string home: Quickshell.env("HOME") readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures` readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos` readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/caelestia` readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/caelestia` readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/caelestia` readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/caelestia` readonly property string imagecache: `${cache}/imagecache` readonly property string notifimagecache: `${imagecache}/notifs` readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(Config.paths.wallpaperDir) readonly property string recsdir: Quickshell.env("CAELESTIA_RECORDINGS_DIR") || `${videos}/Recordings` readonly property string libdir: Quickshell.env("CAELESTIA_LIB_DIR") || "/usr/lib/caelestia" function toLocalFile(path: url): string { path = Qt.resolvedUrl(path); return path.toString() ? CUtils.toLocalFile(path) : ""; } function absolutePath(path: string): string { return toLocalFile(path.replace(/~|(\$({?)HOME(}?))+/, home)); } function shortenHome(path: string): string { return path.replace(home, "~"); } } ================================================ FILE: utils/Searcher.qml ================================================ import Quickshell import "scripts/fzf.js" as Fzf import "scripts/fuzzysort.js" as Fuzzy import QtQuick Singleton { required property list list property string key: "name" property bool useFuzzy: false property var extraOpts: ({}) // Extra stuff for fuzzy property list keys: [key] property list weights: [1] readonly property var fzf: useFuzzy ? [] : new Fzf.Finder(list, Object.assign({ selector }, extraOpts)) readonly property list fuzzyPrepped: useFuzzy ? list.map(e => { const obj = { _item: e }; for (const k of keys) obj[k] = Fuzzy.prepare(e[k]); return obj; }) : [] function transformSearch(search: string): string { return search; } function selector(item: var): string { // Only for fzf return item[key]; } function query(search: string): list { search = transformSearch(search); if (!search) return [...list]; if (useFuzzy) return Fuzzy.go(search, fuzzyPrepped, Object.assign({ all: true, keys, scoreFn: r => weights.reduce((a, w, i) => a + r[i].score * w, 0) }, extraOpts)).map(r => r.obj._item); return fzf.find(search).sort((a, b) => { if (a.score === b.score) return selector(a.item).trim().length - selector(b.item).trim().length; return b.score - a.score; }).map(r => r.item); } } ================================================ FILE: utils/Strings.qml ================================================ pragma Singleton import Quickshell Singleton { property var _regexCache: ({}) function testRegexList(filterList: list, target: string): bool { const regexChecker = /^\^.*\$$/; for (const filter of filterList) { if (regexChecker.test(filter)) { let re = _regexCache[filter]; if (!re) { re = new RegExp(filter); _regexCache[filter] = re; } if (re.test(target)) return true; } else { if (filter === target) return true; } } return false; } } ================================================ FILE: utils/SysInfo.qml ================================================ pragma Singleton import qs.config import qs.utils import Quickshell import Quickshell.Io import QtQuick Singleton { id: root property string osName property string osPrettyName property string osId property list osIdLike property string osLogo: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`) property bool isDefaultLogo: true property string uptime readonly property string user: Quickshell.env("USER") readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP") readonly property string shell: Quickshell.env("SHELL").split("/").pop() FileView { id: osRelease path: "/etc/os-release" onLoaded: { const lines = text().split("\n"); const fd = key => lines.find(l => l.startsWith(`${key}=`))?.split("=")[1].replace(/"/g, "") ?? ""; root.osName = fd("NAME"); root.osPrettyName = fd("PRETTY_NAME"); root.osId = fd("ID"); root.osIdLike = fd("ID_LIKE").split(" "); const logo = Quickshell.iconPath(fd("LOGO"), true); if (Config.general.logo === "caelestia") { root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`); root.isDefaultLogo = true; } else if (Config.general.logo) { root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); root.isDefaultLogo = false; } else if (logo) { root.osLogo = logo; root.isDefaultLogo = false; } } } Connections { function onLogoChanged(): void { osRelease.reload(); } target: Config.general } Timer { running: true repeat: true interval: 15000 onTriggered: fileUptime.reload() } FileView { id: fileUptime path: "/proc/uptime" onLoaded: { const up = parseInt(text().split(" ")[0] ?? 0); const days = Math.floor(up / 86400); const hours = Math.floor((up % 86400) / 3600); const minutes = Math.floor((up % 3600) / 60); let str = ""; if (days > 0) str += `${days} day${days === 1 ? "" : "s"}`; if (hours > 0) str += `${str ? ", " : ""}${hours} hour${hours === 1 ? "" : "s"}`; if (minutes > 0 || !str) str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`; root.uptime = str; } } } ================================================ FILE: utils/scripts/fuzzysort.js ================================================ .pragma library /* https://github.com/farzher/fuzzysort MIT License Copyright (c) 2018 Stephen Kamenar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ var single = (search, target) => { if(!search || !target) return NULL var preparedSearch = getPreparedSearch(search) if(!isPrepared(target)) target = getPrepared(target) var searchBitflags = preparedSearch.bitflags if((searchBitflags & target._bitflags) !== searchBitflags) return NULL return algorithm(preparedSearch, target) } var go = (search, targets, options) => { if(!search) return options?.all ? all(targets, options) : noResults var preparedSearch = getPreparedSearch(search) var searchBitflags = preparedSearch.bitflags var containsSpace = preparedSearch.containsSpace var threshold = denormalizeScore( options?.threshold || 0 ) var limit = options?.limit || INFINITY var resultsLen = 0; var limitedCount = 0 var targetsLen = targets.length function push_result(result) { if(resultsLen < limit) { q.add(result); ++resultsLen } else { ++limitedCount if(result._score > q.peek()._score) q.replaceTop(result) } } // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys] // options.key if(options?.key) { var key = options.key for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] var target = getValue(obj, key) if(!target) continue if(!isPrepared(target)) target = getPrepared(target) if((searchBitflags & target._bitflags) !== searchBitflags) continue var result = algorithm(preparedSearch, target) if(result === NULL) continue if(result._score < threshold) continue result.obj = obj push_result(result) } // options.keys } else if(options?.keys) { var keys = options.keys var keysLen = keys.length outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] { // early out based on bitflags var keysBitflags = 0 for (var keyI = 0; keyI < keysLen; ++keyI) { var key = keys[keyI] var target = getValue(obj, key) if(!target) { tmpTargets[keyI] = noTarget; continue } if(!isPrepared(target)) target = getPrepared(target) tmpTargets[keyI] = target keysBitflags |= target._bitflags } if((searchBitflags & keysBitflags) !== searchBitflags) continue } if(containsSpace) for(let i=0; i -1000) { if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) { var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/ if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp } } if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i] } } if(containsSpace) { for(let i=0; i -1000) { if(score > NEGATIVE_INFINITY) { var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/ if(tmp > score) score = tmp } } if(result._score > score) score = result._score } } objResults.obj = obj objResults._score = score if(options?.scoreFn) { score = options.scoreFn(objResults) if(!score) continue score = denormalizeScore(score) objResults._score = score } if(score < threshold) continue push_result(objResults) } // no keys } else { for(var i = 0; i < targetsLen; ++i) { var target = targets[i] if(!target) continue if(!isPrepared(target)) target = getPrepared(target) if((searchBitflags & target._bitflags) !== searchBitflags) continue var result = algorithm(preparedSearch, target) if(result === NULL) continue if(result._score < threshold) continue push_result(result) } } if(resultsLen === 0) return noResults var results = new Array(resultsLen) for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() results.total = resultsLen + limitedCount return results } // this is written as 1 function instead of 2 for minification. perf seems fine ... // except when minified. the perf is very slow var highlight = (result, open='', close='') => { var callback = typeof open === 'function' ? open : undefined var target = result.target var targetLen = target.length var indexes = result.indexes var highlighted = '' var matchI = 0 var indexesI = 0 var opened = false var parts = [] for(var i = 0; i < targetLen; ++i) { var char = target[i] if(indexes[indexesI] === i) { ++indexesI if(!opened) { opened = true if(callback) { parts.push(highlighted); highlighted = '' } else { highlighted += open } } if(indexesI === indexes.length) { if(callback) { highlighted += char parts.push(callback(highlighted, matchI++)); highlighted = '' parts.push(target.substr(i+1)) } else { highlighted += char + close + target.substr(i+1) } break } } else { if(opened) { opened = false if(callback) { parts.push(callback(highlighted, matchI++)); highlighted = '' } else { highlighted += close } } } highlighted += char } return callback ? parts : highlighted } var prepare = (target) => { if(typeof target === 'number') target = ''+target else if(typeof target !== 'string') target = '' var info = prepareLowerInfo(target) return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags}) } var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() } // Below this point is only internal code // Below this point is only internal code // Below this point is only internal code // Below this point is only internal code class Result { get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) } set ['indexes'](indexes) { return this._indexes = indexes } ['highlight'](open, close) { return highlight(this, open, close) } get ['score']() { return normalizeScore(this._score) } set ['score'](score) { this._score = denormalizeScore(score) } } class KeysResult extends Array { get ['score']() { return normalizeScore(this._score) } set ['score'](score) { this._score = denormalizeScore(score) } } var new_result = (target, options) => { const result = new Result() result['target'] = target result['obj'] = options.obj ?? NULL result._score = options._score ?? NEGATIVE_INFINITY result._indexes = options._indexes ?? [] result._targetLower = options._targetLower ?? '' result._targetLowerCodes = options._targetLowerCodes ?? NULL result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL result._bitflags = options._bitflags ?? 0 return result } var normalizeScore = score => { if(score === NEGATIVE_INFINITY) return 0 if(score > 1) return score return Math.E ** ( ((-score + 1)**.04307 - 1) * -2) } var denormalizeScore = normalizedScore => { if(normalizedScore === 0) return NEGATIVE_INFINITY if(normalizedScore > 1) return normalizedScore return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307) } var prepareSearch = (search) => { if(typeof search === 'number') search = ''+search else if(typeof search !== 'string') search = '' search = search.trim() var info = prepareLowerInfo(search) var spaceSearches = [] if(info.containsSpace) { var searches = search.split(/\s+/) searches = [...new Set(searches)] // distinct for(var i=0; i { if(target.length > 999) return prepare(target) // don't cache huge targets var targetPrepared = preparedCache.get(target) if(targetPrepared !== undefined) return targetPrepared targetPrepared = prepare(target) preparedCache.set(target, targetPrepared) return targetPrepared } var getPreparedSearch = (search) => { if(search.length > 999) return prepareSearch(search) // don't cache huge searches var searchPrepared = preparedSearchCache.get(search) if(searchPrepared !== undefined) return searchPrepared searchPrepared = prepareSearch(search) preparedSearchCache.set(search, searchPrepared) return searchPrepared } var all = (targets, options) => { var results = []; results.total = targets.length // this total can be wrong if some targets are skipped var limit = options?.limit || INFINITY if(options?.key) { for(var i=0;i= limit) return results } } else if(options?.keys) { for(var i=0;i= 0; --keyI) { var target = getValue(obj, options.keys[keyI]) if(!target) { objResults[keyI] = noTarget; continue } if(!isPrepared(target)) target = getPrepared(target) target._score = NEGATIVE_INFINITY target._indexes.len = 0 objResults[keyI] = target } objResults.obj = obj objResults._score = NEGATIVE_INFINITY results.push(objResults); if(results.length >= limit) return results } } else { for(var i=0;i= limit) return results } } return results } var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => { if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch) var searchLower = preparedSearch._lower var searchLowerCodes = preparedSearch.lowerCodes var searchLowerCode = searchLowerCodes[0] var targetLowerCodes = prepared._targetLowerCodes var searchLen = searchLowerCodes.length var targetLen = targetLowerCodes.length var searchI = 0 // where we at var targetI = 0 // where you at var matchesSimpleLen = 0 // very basic fuzzy match; to remove non-matching targets ASAP! // walk through target. find sequential matches. // if all chars aren't found then exit for(;;) { var isMatch = searchLowerCode === targetLowerCodes[targetI] if(isMatch) { matchesSimple[matchesSimpleLen++] = targetI ++searchI; if(searchI === searchLen) break searchLowerCode = searchLowerCodes[searchI] } ++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI } var searchI = 0 var successStrict = false var matchesStrictLen = 0 var nextBeginningIndexes = prepared._nextBeginningIndexes if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target) targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] // Our target string successfully matched all characters in sequence! // Let's try a more advanced and strict test to improve the score // only count it as a match if it's consecutive or a beginning character! var backtrackCount = 0 if(targetI !== targetLen) for(;;) { if(targetI >= targetLen) { // We failed to find a good spot for this search char, go back to the previous search char and force it forward if(searchI <= 0) break // We failed to push chars forward for a better match ++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match --searchI var lastMatch = matchesStrict[--matchesStrictLen] targetI = nextBeginningIndexes[lastMatch] } else { var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] if(isMatch) { matchesStrict[matchesStrictLen++] = targetI ++searchI; if(searchI === searchLen) { successStrict = true; break } ++targetI } else { targetI = nextBeginningIndexes[targetI] } } } // check if it's a substring match var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow var isSubstring = !!~substringIndex var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex // 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 if(isSubstring && !isSubstringBeginning) { for(var i=0; i { var score = 0 var extraMatchGroupCount = 0 for(var i = 1; i < searchLen; ++i) { if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount} } var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1) score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning if(!successStrict) { score *= 1000 } else { // successStrict on a target with too many beginning indexes loses points for being a bad target var uniqueBeginningIndexes = 1 for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ... } score -= (targetLen - searchLen)/2 // penality for longer targets if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex score -= (targetLen - searchLen)/2 // penality for longer targets return score } if(!successStrict) { if(isSubstring) for(var i=0; i { var seen_indexes = new Set() var score = 0 var result = NULL var first_seen_index_last_search = 0 var searches = preparedSearch.spaceSearches var searchesLen = searches.length var changeslen = 0 // Return _nextBeginningIndexes back to its normal state var resetNextBeginningIndexes = () => { for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1] } var hasAtLeast1Match = false for(var i=0; i=0; i--) { if(toReplace !== target._nextBeginningIndexes[i]) break target._nextBeginningIndexes[i] = newBeginningIndex nextBeginningIndexesChanges[changeslen*2 + 0] = i nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace changeslen++ } } } score += result._score / searchesLen allowPartialMatchScores[i] = result._score / searchesLen // dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h if(result._indexes[0] < first_seen_index_last_search) { score -= (first_seen_index_last_search - result._indexes[0]) * 2 } first_seen_index_last_search = result._indexes[0] for(var j=0; j score) { if(allowPartialMatch) { for(var i=0; i str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '') var prepareLowerInfo = (str) => { str = remove_accents(str) var strLen = str.length var lower = str.toLowerCase() var lowerCodes = [] // new Array(strLen) sparse array is too slow var bitflags = 0 var containsSpace = false // space isn't stored in bitflags because of how searching with a space works for(var i = 0; i < strLen; ++i) { var lowerCode = lowerCodes[i] = lower.charCodeAt(i) if(lowerCode === 32) { containsSpace = true continue // it's important that we don't set any bitflags for space } var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet : lowerCode>=48&&lowerCode<=57 ? 26 // numbers // 3 bits available : lowerCode<=127 ? 30 // other ascii : 31 // other utf8 bitflags |= 1< { var targetLen = target.length var beginningIndexes = []; var beginningIndexesLen = 0 var wasUpper = false var wasAlphanum = false for(var i = 0; i < targetLen; ++i) { var targetCode = target.charCodeAt(i) var isUpper = targetCode>=65&&targetCode<=90 var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum wasUpper = isUpper wasAlphanum = isAlphanum if(isBeginning) beginningIndexes[beginningIndexesLen++] = i } return beginningIndexes } var prepareNextBeginningIndexes = (target) => { target = remove_accents(target) var targetLen = target.length var beginningIndexes = prepareBeginningIndexes(target) var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow var lastIsBeginning = beginningIndexes[0] var lastIsBeginningI = 0 for(var i = 0; i < targetLen; ++i) { if(lastIsBeginning > i) { nextBeginningIndexes[i] = lastIsBeginning } else { lastIsBeginning = beginningIndexes[++lastIsBeginningI] nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning } } return nextBeginningIndexes } var preparedCache = new Map() var preparedSearchCache = new Map() // the theory behind these being globals is to reduce garbage collection by not making new arrays var matchesSimple = []; var matchesStrict = [] var 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 var keysSpacesBestScores = []; var allowPartialMatchScores = [] var tmpTargets = []; var tmpResults = [] // prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] // prop = 'key1.key2' 10ms // prop = ['key1', 'key2'] 27ms // prop = obj => obj.tags.join() ??ms var getValue = (obj, prop) => { var tmp = obj[prop]; if(tmp !== undefined) return tmp if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower var segs = prop if(!Array.isArray(prop)) segs = prop.split('.') var len = segs.length var i = -1 while (obj && (++i < len)) obj = obj[segs[i]] return obj } var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' } var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY var noResults = []; noResults.total = 0 var NULL = null var noTarget = prepare('') // Hacked version of https://github.com/lemire/FastPriorityQueue.js var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score>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>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} var q = fastpriorityqueue() // reuse this ================================================ FILE: utils/scripts/fzf.js ================================================ .pragma library /* https://github.com/ajitid/fzf-for-js BSD 3-Clause License Copyright (c) 2021, Ajit All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const normalized = { 216: "O", 223: "s", 248: "o", 273: "d", 295: "h", 305: "i", 320: "l", 322: "l", 359: "t", 383: "s", 384: "b", 385: "B", 387: "b", 390: "O", 392: "c", 393: "D", 394: "D", 396: "d", 398: "E", 400: "E", 402: "f", 403: "G", 407: "I", 409: "k", 410: "l", 412: "M", 413: "N", 414: "n", 415: "O", 421: "p", 427: "t", 429: "t", 430: "T", 434: "V", 436: "y", 438: "z", 477: "e", 485: "g", 544: "N", 545: "d", 549: "z", 564: "l", 565: "n", 566: "t", 567: "j", 570: "A", 571: "C", 572: "c", 573: "L", 574: "T", 575: "s", 576: "z", 579: "B", 580: "U", 581: "V", 582: "E", 583: "e", 584: "J", 585: "j", 586: "Q", 587: "q", 588: "R", 589: "r", 590: "Y", 591: "y", 592: "a", 593: "a", 595: "b", 596: "o", 597: "c", 598: "d", 599: "d", 600: "e", 603: "e", 604: "e", 605: "e", 606: "e", 607: "j", 608: "g", 609: "g", 610: "G", 613: "h", 614: "h", 616: "i", 618: "I", 619: "l", 620: "l", 621: "l", 623: "m", 624: "m", 625: "m", 626: "n", 627: "n", 628: "N", 629: "o", 633: "r", 634: "r", 635: "r", 636: "r", 637: "r", 638: "r", 639: "r", 640: "R", 641: "R", 642: "s", 647: "t", 648: "t", 649: "u", 651: "v", 652: "v", 653: "w", 654: "y", 655: "Y", 656: "z", 657: "z", 663: "c", 665: "B", 666: "e", 667: "G", 668: "H", 669: "j", 670: "k", 671: "L", 672: "q", 686: "h", 867: "a", 868: "e", 869: "i", 870: "o", 871: "u", 872: "c", 873: "d", 874: "h", 875: "m", 876: "r", 877: "t", 878: "v", 879: "x", 7424: "A", 7427: "B", 7428: "C", 7429: "D", 7431: "E", 7432: "e", 7433: "i", 7434: "J", 7435: "K", 7436: "L", 7437: "M", 7438: "N", 7439: "O", 7440: "O", 7441: "o", 7442: "o", 7443: "o", 7446: "o", 7447: "o", 7448: "P", 7449: "R", 7450: "R", 7451: "T", 7452: "U", 7453: "u", 7454: "u", 7455: "m", 7456: "V", 7457: "W", 7458: "Z", 7522: "i", 7523: "r", 7524: "u", 7525: "v", 7834: "a", 7835: "s", 8305: "i", 8341: "h", 8342: "k", 8343: "l", 8344: "m", 8345: "n", 8346: "p", 8347: "s", 8348: "t", 8580: "c" }; for (let i = "\u0300".codePointAt(0); i <= "\u036F".codePointAt(0); ++i) { const diacritic = String.fromCodePoint(i); for (const asciiChar of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") { const withDiacritic = (asciiChar + diacritic).normalize(); const withDiacriticCodePoint = withDiacritic.codePointAt(0); if (withDiacriticCodePoint > 126) { normalized[withDiacriticCodePoint] = asciiChar; } } } const ranges = { a: [7844, 7863], e: [7870, 7879], o: [7888, 7907], u: [7912, 7921] }; for (const lowerChar of Object.keys(ranges)) { const upperChar = lowerChar.toUpperCase(); for (let i = ranges[lowerChar][0]; i <= ranges[lowerChar][1]; ++i) { normalized[i] = i % 2 === 0 ? upperChar : lowerChar; } } function normalizeRune(rune) { if (rune < 192 || rune > 8580) { return rune; } const normalizedChar = normalized[rune]; if (normalizedChar !== void 0) return normalizedChar.codePointAt(0); return rune; } function toShort(number) { return number; } function toInt(number) { return number; } function maxInt16(num1, num2) { return num1 > num2 ? num1 : num2; } const strToRunes = (str) => str.split("").map((s) => s.codePointAt(0)); const runesToStr = (runes) => runes.map((r) => String.fromCodePoint(r)).join(""); const whitespaceRunes = new Set( " \f\n\r \v\xA0\u1680\u2028\u2029\u202F\u205F\u3000\uFEFF".split("").map((v) => v.codePointAt(0)) ); for (let codePoint = "\u2000".codePointAt(0); codePoint <= "\u200A".codePointAt(0); codePoint++) { whitespaceRunes.add(codePoint); } const isWhitespace = (rune) => whitespaceRunes.has(rune); const whitespacesAtStart = (runes) => { let whitespaces = 0; for (const rune of runes) { if (isWhitespace(rune)) whitespaces++; else break; } return whitespaces; }; const whitespacesAtEnd = (runes) => { let whitespaces = 0; for (let i = runes.length - 1; i >= 0; i--) { if (isWhitespace(runes[i])) whitespaces++; else break; } return whitespaces; }; const MAX_ASCII = "\x7F".codePointAt(0); const CAPITAL_A_RUNE = "A".codePointAt(0); const CAPITAL_Z_RUNE = "Z".codePointAt(0); const SMALL_A_RUNE = "a".codePointAt(0); const SMALL_Z_RUNE = "z".codePointAt(0); const NUMERAL_ZERO_RUNE = "0".codePointAt(0); const NUMERAL_NINE_RUNE = "9".codePointAt(0); function indexAt(index, max, forward) { if (forward) { return index; } return max - index - 1; } const 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; function createPosSet(withPos) { if (withPos) { return /* @__PURE__ */ new Set(); } return null; } function alloc16(offset, slab2, size) { if (slab2 !== null && slab2.i16.length > offset + size) { const subarray = slab2.i16.subarray(offset, offset + size); return [offset + size, subarray]; } return [offset, new Int16Array(size)]; } function alloc32(offset, slab2, size) { if (slab2 !== null && slab2.i32.length > offset + size) { const subarray = slab2.i32.subarray(offset, offset + size); return [offset + size, subarray]; } return [offset, new Int32Array(size)]; } function charClassOfAscii(rune) { if (rune >= SMALL_A_RUNE && rune <= SMALL_Z_RUNE) { return 1; } else if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { return 2; } else if (rune >= NUMERAL_ZERO_RUNE && rune <= NUMERAL_NINE_RUNE) { return 4; } else { return 0; } } function charClassOfNonAscii(rune) { const char = String.fromCodePoint(rune); if (char !== char.toUpperCase()) { return 1; } else if (char !== char.toLowerCase()) { return 2; } else if (char.match(/\p{Number}/gu) !== null) { return 4; } else if (char.match(/\p{Letter}/gu) !== null) { return 3; } return 0; } function charClassOf(rune) { if (rune <= MAX_ASCII) { return charClassOfAscii(rune); } return charClassOfNonAscii(rune); } function bonusFor(prevClass, currClass) { if (prevClass === 0 && currClass !== 0) { return BONUS_BOUNDARY; } else if (prevClass === 1 && currClass === 2 || prevClass !== 4 && currClass === 4) { return BONUS_CAMEL_123; } else if (currClass === 0) { return BONUS_NON_WORD; } return 0; } function bonusAt(input, idx) { if (idx === 0) { return BONUS_BOUNDARY; } return bonusFor(charClassOf(input[idx - 1]), charClassOf(input[idx])); } function trySkip(input, caseSensitive, char, from) { let rest = input.slice(from); let idx = rest.indexOf(char); if (idx === 0) { return from; } if (!caseSensitive && char >= SMALL_A_RUNE && char <= SMALL_Z_RUNE) { if (idx > 0) { rest = rest.slice(0, idx); } const uidx = rest.indexOf(char - 32); if (uidx >= 0) { idx = uidx; } } if (idx < 0) { return -1; } return from + idx; } function isAscii(runes) { for (const rune of runes) { if (rune >= 128) { return false; } } return true; } function asciiFuzzyIndex(input, pattern, caseSensitive) { if (!isAscii(input)) { return 0; } if (!isAscii(pattern)) { return -1; } let firstIdx = 0, idx = 0; for (let pidx = 0; pidx < pattern.length; pidx++) { idx = trySkip(input, caseSensitive, pattern[pidx], idx); if (idx < 0) { return -1; } if (pidx === 0 && idx > 0) { firstIdx = idx - 1; } idx++; } return firstIdx; } const fuzzyMatchV2 = (caseSensitive, normalize, forward, input, pattern, withPos, slab2) => { const M = pattern.length; if (M === 0) { return [{ start: 0, end: 0, score: 0 }, createPosSet(withPos)]; } const N = input.length; if (slab2 !== null && N * M > slab2.i16.length) { return fuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos); } const idx = asciiFuzzyIndex(input, pattern, caseSensitive); if (idx < 0) { return [{ start: -1, end: -1, score: 0 }, null]; } let offset16 = 0, offset32 = 0, H0 = null, C0 = null, B = null, F = null; [offset16, H0] = alloc16(offset16, slab2, N); [offset16, C0] = alloc16(offset16, slab2, N); [offset16, B] = alloc16(offset16, slab2, N); [offset32, F] = alloc32(offset32, slab2, M); const [, T] = alloc32(offset32, slab2, N); for (let i = 0; i < T.length; i++) { T[i] = input[i]; } let maxScore = toShort(0), maxScorePos = 0; let pidx = 0, lastIdx = 0; const pchar0 = pattern[0]; let pchar = pattern[0], prevH0 = toShort(0), prevCharClass = 0, inGap = false; let Tsub = T.subarray(idx); 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); for (let [off, char] of Tsub.entries()) { let charClass = null; if (char <= MAX_ASCII) { charClass = charClassOfAscii(char); if (!caseSensitive && charClass === 2) { char += 32; } } else { charClass = charClassOfNonAscii(char); if (!caseSensitive && charClass === 2) { char = String.fromCodePoint(char).toLowerCase().codePointAt(0); } if (normalize) { char = normalizeRune(char); } } Tsub[off] = char; const bonus = bonusFor(prevCharClass, charClass); Bsub[off] = bonus; prevCharClass = charClass; if (char === pchar) { if (pidx < M) { F[pidx] = toInt(idx + off); pidx++; pchar = pattern[Math.min(pidx, M - 1)]; } lastIdx = idx + off; } if (char === pchar0) { const score = SCORE_MATCH + bonus * BONUS_FIRST_CHAR_MULTIPLIER; H0sub[off] = score; C0sub[off] = 1; if (M === 1 && (forward && score > maxScore || !forward && score >= maxScore)) { maxScore = score; maxScorePos = idx + off; if (forward && bonus === BONUS_BOUNDARY) { break; } } inGap = false; } else { if (inGap) { H0sub[off] = maxInt16(prevH0 + SCORE_GAP_EXTENTION, 0); } else { H0sub[off] = maxInt16(prevH0 + SCORE_GAP_START, 0); } C0sub[off] = 0; inGap = true; } prevH0 = H0sub[off]; } if (pidx !== M) { return [{ start: -1, end: -1, score: 0 }, null]; } if (M === 1) { const result = { start: maxScorePos, end: maxScorePos + 1, score: maxScore }; if (!withPos) { return [result, null]; } const pos2 = /* @__PURE__ */ new Set(); pos2.add(maxScorePos); return [result, pos2]; } const f0 = F[0]; const width = lastIdx - f0 + 1; let H = null; [offset16, H] = alloc16(offset16, slab2, width * M); { const toCopy = H0.subarray(f0, lastIdx + 1); for (const [i, v] of toCopy.entries()) { H[i] = v; } } let [, C] = alloc16(offset16, slab2, width * M); { const toCopy = C0.subarray(f0, lastIdx + 1); for (const [i, v] of toCopy.entries()) { C[i] = v; } } const Fsub = F.subarray(1); const Psub = pattern.slice(1).slice(0, Fsub.length); for (const [off, f] of Fsub.entries()) { let inGap2 = false; 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); Hleft[0] = 0; for (const [off2, char] of Tsub2.entries()) { const col = off2 + f; let s1 = 0, s2 = 0, consecutive = 0; if (inGap2) { s2 = Hleft[off2] + SCORE_GAP_EXTENTION; } else { s2 = Hleft[off2] + SCORE_GAP_START; } if (pchar2 === char) { s1 = Hdiag[off2] + SCORE_MATCH; let b = Bsub2[off2]; consecutive = Cdiag[off2] + 1; if (b === BONUS_BOUNDARY) { consecutive = 1; } else if (consecutive > 1) { b = maxInt16(b, maxInt16(BONUS_CONSECUTIVE, B[col - consecutive + 1])); } if (s1 + b < s2) { s1 += Bsub2[off2]; consecutive = 0; } else { s1 += b; } } Csub[off2] = consecutive; inGap2 = s1 < s2; const score = maxInt16(maxInt16(s1, s2), 0); if (pidx2 === M - 1 && (forward && score > maxScore || !forward && score >= maxScore)) { maxScore = score; maxScorePos = col; } Hsub[off2] = score; } } const pos = createPosSet(withPos); let j = f0; if (withPos && pos !== null) { let i = M - 1; j = maxScorePos; let preferMatch = true; while (true) { const I = i * width, j0 = j - f0, s = H[I + j0]; let s1 = 0, s2 = 0; if (i > 0 && j >= F[i]) { s1 = H[I - width + j0 - 1]; } if (j > F[i]) { s2 = H[I + j0 - 1]; } if (s > s1 && (s > s2 || s === s2 && preferMatch)) { pos.add(j); if (i === 0) { break; } i--; } preferMatch = C[I + j0] > 1 || I + width + j0 + 1 < C.length && C[I + width + j0 + 1] > 0; j--; } } return [{ start: j, end: maxScorePos + 1, score: maxScore }, pos]; }; function calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) { let pidx = 0, score = 0, inGap = false, consecutive = 0, firstBonus = toShort(0); const pos = createPosSet(withPos); let prevCharClass = 0; if (sidx > 0) { prevCharClass = charClassOf(text[sidx - 1]); } for (let idx = sidx; idx < eidx; idx++) { let rune = text[idx]; const charClass = charClassOf(rune); if (!caseSensitive) { if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { rune += 32; } else if (rune > MAX_ASCII) { rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); } } if (normalize) { rune = normalizeRune(rune); } if (rune === pattern[pidx]) { if (withPos && pos !== null) { pos.add(idx); } score += SCORE_MATCH; let bonus = bonusFor(prevCharClass, charClass); if (consecutive === 0) { firstBonus = bonus; } else { if (bonus === BONUS_BOUNDARY) { firstBonus = bonus; } bonus = maxInt16(maxInt16(bonus, firstBonus), BONUS_CONSECUTIVE); } if (pidx === 0) { score += bonus * BONUS_FIRST_CHAR_MULTIPLIER; } else { score += bonus; } inGap = false; consecutive++; pidx++; } else { if (inGap) { score += SCORE_GAP_EXTENTION; } else { score += SCORE_GAP_START; } inGap = true; consecutive = 0; firstBonus = 0; } prevCharClass = charClass; } return [score, pos]; } function fuzzyMatchV1(caseSensitive, normalize, forward, text, pattern, withPos, slab2) { if (pattern.length === 0) { return [{ start: 0, end: 0, score: 0 }, null]; } if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) { return [{ start: -1, end: -1, score: 0 }, null]; } let pidx = 0, sidx = -1, eidx = -1; const lenRunes = text.length; const lenPattern = pattern.length; for (let index = 0; index < lenRunes; index++) { let rune = text[indexAt(index, lenRunes, forward)]; if (!caseSensitive) { if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { rune += 32; } else if (rune > MAX_ASCII) { rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); } } if (normalize) { rune = normalizeRune(rune); } const pchar = pattern[indexAt(pidx, lenPattern, forward)]; if (rune === pchar) { if (sidx < 0) { sidx = index; } pidx++; if (pidx === lenPattern) { eidx = index + 1; break; } } } if (sidx >= 0 && eidx >= 0) { pidx--; for (let index = eidx - 1; index >= sidx; index--) { const tidx = indexAt(index, lenRunes, forward); let rune = text[tidx]; if (!caseSensitive) { if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { rune += 32; } else if (rune > MAX_ASCII) { rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); } } const pidx_ = indexAt(pidx, lenPattern, forward); const pchar = pattern[pidx_]; if (rune === pchar) { pidx--; if (pidx < 0) { sidx = index; break; } } } if (!forward) { const sidxTemp = sidx; sidx = lenRunes - eidx; eidx = lenRunes - sidxTemp; } const [score, pos] = calculateScore( caseSensitive, normalize, text, pattern, sidx, eidx, withPos ); return [{ start: sidx, end: eidx, score }, pos]; } return [{ start: -1, end: -1, score: 0 }, null]; }; const exactMatchNaive = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { if (pattern.length === 0) { return [{ start: 0, end: 0, score: 0 }, null]; } const lenRunes = text.length; const lenPattern = pattern.length; if (lenRunes < lenPattern) { return [{ start: -1, end: -1, score: 0 }, null]; } if (asciiFuzzyIndex(text, pattern, caseSensitive) < 0) { return [{ start: -1, end: -1, score: 0 }, null]; } let pidx = 0; let bestPos = -1, bonus = toShort(0), bestBonus = toShort(-1); for (let index = 0; index < lenRunes; index++) { const index_ = indexAt(index, lenRunes, forward); let rune = text[index_]; if (!caseSensitive) { if (rune >= CAPITAL_A_RUNE && rune <= CAPITAL_Z_RUNE) { rune += 32; } else if (rune > MAX_ASCII) { rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); } } if (normalize) { rune = normalizeRune(rune); } const pidx_ = indexAt(pidx, lenPattern, forward); const pchar = pattern[pidx_]; if (pchar === rune) { if (pidx_ === 0) { bonus = bonusAt(text, index_); } pidx++; if (pidx === lenPattern) { if (bonus > bestBonus) { bestPos = index; bestBonus = bonus; } if (bonus === BONUS_BOUNDARY) { break; } index -= pidx - 1; pidx = 0; bonus = 0; } } else { index -= pidx; pidx = 0; bonus = 0; } } if (bestPos >= 0) { let sidx = 0, eidx = 0; if (forward) { sidx = bestPos - lenPattern + 1; eidx = bestPos + 1; } else { sidx = lenRunes - (bestPos + 1); eidx = lenRunes - (bestPos - lenPattern + 1); } const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false); return [{ start: sidx, end: eidx, score }, null]; } return [{ start: -1, end: -1, score: 0 }, null]; }; const prefixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { if (pattern.length === 0) { return [{ start: 0, end: 0, score: 0 }, null]; } let trimmedLen = 0; if (!isWhitespace(pattern[0])) { trimmedLen = whitespacesAtStart(text); } if (text.length - trimmedLen < pattern.length) { return [{ start: -1, end: -1, score: 0 }, null]; } for (const [index, r] of pattern.entries()) { let rune = text[trimmedLen + index]; if (!caseSensitive) { rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); } if (normalize) { rune = normalizeRune(rune); } if (rune !== r) { return [{ start: -1, end: -1, score: 0 }, null]; } } const lenPattern = pattern.length; const [score] = calculateScore( caseSensitive, normalize, text, pattern, trimmedLen, trimmedLen + lenPattern, false ); return [{ start: trimmedLen, end: trimmedLen + lenPattern, score }, null]; }; const suffixMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { const lenRunes = text.length; let trimmedLen = lenRunes; if (pattern.length === 0 || !isWhitespace(pattern[pattern.length - 1])) { trimmedLen -= whitespacesAtEnd(text); } if (pattern.length === 0) { return [{ start: trimmedLen, end: trimmedLen, score: 0 }, null]; } const diff = trimmedLen - pattern.length; if (diff < 0) { return [{ start: -1, end: -1, score: 0 }, null]; } for (const [index, r] of pattern.entries()) { let rune = text[index + diff]; if (!caseSensitive) { rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); } if (normalize) { rune = normalizeRune(rune); } if (rune !== r) { return [{ start: -1, end: -1, score: 0 }, null]; } } const lenPattern = pattern.length; const sidx = trimmedLen - lenPattern; const eidx = trimmedLen; const [score] = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false); return [{ start: sidx, end: eidx, score }, null]; }; const equalMatch = (caseSensitive, normalize, forward, text, pattern, withPos, slab2) => { const lenPattern = pattern.length; if (lenPattern === 0) { return [{ start: -1, end: -1, score: 0 }, null]; } let trimmedLen = 0; if (!isWhitespace(pattern[0])) { trimmedLen = whitespacesAtStart(text); } let trimmedEndLen = 0; if (!isWhitespace(pattern[lenPattern - 1])) { trimmedEndLen = whitespacesAtEnd(text); } if (text.length - trimmedLen - trimmedEndLen != lenPattern) { return [{ start: -1, end: -1, score: 0 }, null]; } let match = true; if (normalize) { const runes = text; for (const [idx, pchar] of pattern.entries()) { let rune = runes[trimmedLen + idx]; if (!caseSensitive) { rune = String.fromCodePoint(rune).toLowerCase().codePointAt(0); } if (normalizeRune(pchar) !== normalizeRune(rune)) { match = false; break; } } } else { let runesStr = runesToStr(text).substring(trimmedLen, text.length - trimmedEndLen); if (!caseSensitive) { runesStr = runesStr.toLowerCase(); } match = runesStr === runesToStr(pattern); } if (match) { return [ { start: trimmedLen, end: trimmedLen + lenPattern, score: (SCORE_MATCH + BONUS_BOUNDARY) * lenPattern + (BONUS_FIRST_CHAR_MULTIPLIER - 1) * BONUS_BOUNDARY }, null ]; } return [{ start: -1, end: -1, score: 0 }, null]; }; const SLAB_16_SIZE = 100 * 1024; const SLAB_32_SIZE = 2048; function makeSlab(size16, size32) { return { i16: new Int16Array(size16), i32: new Int32Array(size32) }; } const slab = makeSlab(SLAB_16_SIZE, SLAB_32_SIZE); var TermType = /* @__PURE__ */ ((TermType2) => { TermType2[TermType2["Fuzzy"] = 0] = "Fuzzy"; TermType2[TermType2["Exact"] = 1] = "Exact"; TermType2[TermType2["Prefix"] = 2] = "Prefix"; TermType2[TermType2["Suffix"] = 3] = "Suffix"; TermType2[TermType2["Equal"] = 4] = "Equal"; return TermType2; })(TermType || {}); const termTypeMap = { [0]: fuzzyMatchV2, [1]: exactMatchNaive, [2]: prefixMatch, [3]: suffixMatch, [4]: equalMatch }; function buildPatternForExtendedMatch(fuzzy, caseMode, normalize, str) { let cacheable = true; str = str.trimLeft(); { const trimmedAtRightStr = str.trimRight(); if (trimmedAtRightStr.endsWith("\\") && str[trimmedAtRightStr.length] === " ") { str = trimmedAtRightStr + " "; } else { str = trimmedAtRightStr; } } let sortable = false; let termSets = []; termSets = parseTerms(fuzzy, caseMode, normalize, str); Loop: for (const termSet of termSets) { for (const [idx, term] of termSet.entries()) { if (!term.inv) { sortable = true; } if (!cacheable || idx > 0 || term.inv || fuzzy && term.typ !== 0 || !fuzzy && term.typ !== 1) { cacheable = false; if (sortable) { break Loop; } } } } return { str, termSets, sortable, cacheable, fuzzy }; } function parseTerms(fuzzy, caseMode, normalize, str) { str = str.replace(/\\ /g, " "); const tokens = str.split(/ +/); const sets = []; let set = []; let switchSet = false; let afterBar = false; for (const token of tokens) { let typ = 0, inv = false, text = token.replace(/\t/g, " "); const lowerText = text.toLowerCase(); const caseSensitive = caseMode === "case-sensitive" || caseMode === "smart-case" && text !== lowerText; const normalizeTerm = normalize && lowerText === runesToStr(strToRunes(lowerText).map(normalizeRune)); if (!caseSensitive) { text = lowerText; } if (!fuzzy) { typ = 1; } if (set.length > 0 && !afterBar && text === "|") { switchSet = false; afterBar = true; continue; } afterBar = false; if (text.startsWith("!")) { inv = true; typ = 1; text = text.substring(1); } if (text !== "$" && text.endsWith("$")) { typ = 3; text = text.substring(0, text.length - 1); } if (text.startsWith("'")) { if (fuzzy && !inv) { typ = 1; } else { typ = 0; } text = text.substring(1); } else if (text.startsWith("^")) { if (typ === 3) { typ = 4; } else { typ = 2; } text = text.substring(1); } if (text.length > 0) { if (switchSet) { sets.push(set); set = []; } let textRunes = strToRunes(text); if (normalizeTerm) { textRunes = textRunes.map(normalizeRune); } set.push({ typ, inv, text: textRunes, caseSensitive, normalize: normalizeTerm }); switchSet = true; } } if (set.length > 0) { sets.push(set); } return sets; } const buildPatternForBasicMatch = (query, casing, normalize) => { let caseSensitive = false; switch (casing) { case "smart-case": if (query.toLowerCase() !== query) { caseSensitive = true; } break; case "case-sensitive": caseSensitive = true; break; case "case-insensitive": query = query.toLowerCase(); caseSensitive = false; break; } let queryRunes = strToRunes(query); if (normalize) { queryRunes = queryRunes.map(normalizeRune); } return { queryRunes, caseSensitive }; }; function iter(algoFn, tokens, caseSensitive, normalize, forward, pattern, slab2) { for (const part of tokens) { const [res, pos] = algoFn(caseSensitive, normalize, forward, part.text, pattern, true, slab2); if (res.start >= 0) { const sidx = res.start + part.prefixLength; const eidx = res.end + part.prefixLength; if (pos !== null) { const newPos = /* @__PURE__ */ new Set(); pos.forEach((v) => newPos.add(part.prefixLength + v)); return [[sidx, eidx], res.score, newPos]; } return [[sidx, eidx], res.score, pos]; } } return [[-1, -1], 0, null]; } function computeExtendedMatch(text, pattern, fuzzyAlgo, forward) { const input = [ { text, prefixLength: 0 } ]; const offsets = []; let totalScore = 0; const allPos = /* @__PURE__ */ new Set(); for (const termSet of pattern.termSets) { let offset = [0, 0]; let currentScore = 0; let matched = false; for (const term of termSet) { let algoFn = termTypeMap[term.typ]; if (term.typ === TermType.Fuzzy) { algoFn = fuzzyAlgo; } const [off, score, pos] = iter( algoFn, input, term.caseSensitive, term.normalize, forward, term.text, slab ); const sidx = off[0]; if (sidx >= 0) { if (term.inv) { continue; } offset = off; currentScore = score; matched = true; if (pos !== null) { pos.forEach((v) => allPos.add(v)); } else { for (let idx = off[0]; idx < off[1]; ++idx) { allPos.add(idx); } } break; } else if (term.inv) { offset = [0, 0]; currentScore = 0; matched = true; continue; } } if (matched) { offsets.push(offset); totalScore += currentScore; } } return { offsets, totalScore, allPos }; } function getResultFromScoreMap(scoreMap, limit) { const scoresInDesc = Object.keys(scoreMap).map((v) => parseInt(v, 10)).sort((a, b) => b - a); let result = []; for (const score of scoresInDesc) { result = result.concat(scoreMap[score]); if (result.length >= limit) { break; } } return result; } function getBasicMatchIter(scoreMap, queryRunes, caseSensitive) { return (idx) => { const itemRunes = this.runesList[idx]; if (queryRunes.length > itemRunes.length) return; let [match, positions] = this.algoFn( caseSensitive, this.opts.normalize, this.opts.forward, itemRunes, queryRunes, true, slab ); if (match.start === -1) return; if (this.opts.fuzzy === false) { positions = /* @__PURE__ */ new Set(); for (let position = match.start; position < match.end; ++position) { positions.add(position); } } const scoreKey = this.opts.sort ? match.score : 0; if (scoreMap[scoreKey] === void 0) { scoreMap[scoreKey] = []; } scoreMap[scoreKey].push(Object.assign({ item: this.items[idx], positions: positions != null ? positions : /* @__PURE__ */ new Set() }, match)); }; } function getExtendedMatchIter(scoreMap, pattern) { return (idx) => { const runes = this.runesList[idx]; const match = computeExtendedMatch(runes, pattern, this.algoFn, this.opts.forward); if (match.offsets.length !== pattern.termSets.length) return; let sidx = -1, eidx = -1; if (match.allPos.size > 0) { sidx = Math.min(...match.allPos); eidx = Math.max(...match.allPos) + 1; } const scoreKey = this.opts.sort ? match.totalScore : 0; if (scoreMap[scoreKey] === void 0) { scoreMap[scoreKey] = []; } scoreMap[scoreKey].push({ score: match.totalScore, item: this.items[idx], positions: match.allPos, start: sidx, end: eidx }); }; } function basicMatch(query) { const { queryRunes, caseSensitive } = buildPatternForBasicMatch( query, this.opts.casing, this.opts.normalize ); const scoreMap = {}; const iter2 = getBasicMatchIter.bind(this)( scoreMap, queryRunes, caseSensitive ); for (let i = 0, len = this.runesList.length; i < len; ++i) { iter2(i); } return getResultFromScoreMap(scoreMap, this.opts.limit); } function extendedMatch(query) { const pattern = buildPatternForExtendedMatch( Boolean(this.opts.fuzzy), this.opts.casing, this.opts.normalize, query ); const scoreMap = {}; const iter2 = getExtendedMatchIter.bind(this)(scoreMap, pattern); for (let i = 0, len = this.runesList.length; i < len; ++i) { iter2(i); } return getResultFromScoreMap(scoreMap, this.opts.limit); } const defaultOpts = { limit: Infinity, selector: (v) => v, casing: "smart-case", normalize: true, fuzzy: "v2", tiebreakers: [], sort: true, forward: true, match: basicMatch }; class Finder { constructor(list, ...optionsTuple) { this.opts = Object.assign(defaultOpts, optionsTuple[0]); this.items = list; this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize())); this.algoFn = exactMatchNaive; switch (this.opts.fuzzy) { case "v2": this.algoFn = fuzzyMatchV2; break; case "v1": this.algoFn = fuzzyMatchV1; break; } } find(query) { if (query.length === 0 || this.items.length === 0) return this.items.slice(0, this.opts.limit).map(createResultItemWithEmptyPos); query = query.normalize(); let result = this.opts.match.bind(this)(query); return postProcessResultItems(result, this.opts); } } function createResultItemWithEmptyPos(item) { return ({ item, start: -1, end: -1, score: 0, positions: /* @__PURE__ */ new Set() }) }; function postProcessResultItems(result, opts) { if (opts.sort) { const { selector } = opts; result.sort((a, b) => { if (a.score === b.score) { for (const tiebreaker of opts.tiebreakers) { const diff = tiebreaker(a, b, selector); if (diff !== 0) { return diff; } } } return 0; }); } if (Number.isFinite(opts.limit)) { result.splice(opts.limit); } return result; } function byLengthAsc(a, b, selector) { return selector(a.item).length - selector(b.item).length; } function byStartAsc(a, b) { return a.start - b.start; } ================================================ FILE: utils/scripts/lrcparser.js ================================================ function parseLrc(text) { if (!text) return []; let lines = text.split("\n"); let result = []; let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; // Blacklist for credits/metadata often found in NetEase lyrics const creditKeywords = [ "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" ]; for (let line of lines) { timeRegex.lastIndex = 0; let matches = []; let match; while ((match = timeRegex.exec(line)) !== null) { matches.push(match); } if (matches.length === 0) continue; let lyric = line.replace(timeRegex, "").trim(); let min = parseInt(matches[0][1]); let sec = parseFloat(matches[0][2]); let totalTime = min * 60 + sec; // Only filter credits if they appear in the first 20 seconds if (totalTime < 20) { let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { continue; } } for (let match of matches) { let min = parseInt(match[1]); let sec = parseFloat(match[2]); result.push({ time: min * 60 + sec, text: lyric }); } } result.sort((a, b) => a.time - b.time); return result; } function getCurrentLine(lyrics, position) { const epsilon = 0.1; // 100ms tolerance for (let i = lyrics.length - 1; i >= 0; i--) { if ((position + epsilon) >= lyrics[i].time) { return i; } } return -1; }