diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f87a347 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/_titanoboa +/podman_scp* diff --git a/.github/workflows/build-disk.yml b/.github/workflows/build-disk.yml index 356b1cd..0663f58 100644 --- a/.github/workflows/build-disk.yml +++ b/.github/workflows/build-disk.yml @@ -53,12 +53,15 @@ jobs: - name: Build ISO id: build - uses: ublue-os/titanoboa@main + uses: binarypie-dev/titanoboa@main with: image-ref: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.variant.tag }} iso-dest: ${{ github.workspace }}/output.iso builder-distro: fedora - hook-pre-initramfs: ${{ github.workspace }}/iso_files/hide_hyprland_session.sh + livesys: true + livesys-repo: binarypie/hypercube + flatpaks-list: ${{ github.workspace }}/flatpaks/system-flatpaks.list + hook-post-rootfs: ${{ github.workspace }}/iso_files/hook-post-rootfs.sh - name: Rename ISO and generate checksum id: rename diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 017968b..12ff52c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,21 +5,22 @@ on: branches: - main schedule: - - cron: '05 10 * * *' # 10:05am UTC everyday + - cron: "05 10 * * *" # 10:05am UTC everyday push: branches: - main paths-ignore: - - '**/README.md' + - "**/README.md" workflow_dispatch: env: - IMAGE_DESC: "Hypercube - A Hyprland-focused bootc image built on Bluefin-DX" - IMAGE_KEYWORDS: "bootc,ublue,universal-blue,hyprland,hypercube,wayland" + IMAGE_DESC: "Hypercube - A developer workstation with Hyprland built on Universal Blue" + IMAGE_KEYWORDS: "bootc,ublue,universal-blue,hyprland,hypercube,wayland,developer" IMAGE_LOGO_URL: "https://raw.githubusercontent.com/${{ github.repository_owner }}/${{ github.event.repository.name }}/main/branding/hypercube-logo.png" IMAGE_NAME: "${{ github.event.repository.name }}" IMAGE_REGISTRY: "ghcr.io/${{ github.repository_owner }}" DEFAULT_TAG: "latest" + FEDORA_VERSION: "43" concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} @@ -40,10 +41,12 @@ jobs: matrix: variant: - flavor: "main" - base_image: "ghcr.io/ublue-os/bluefin-dx:stable-daily" + source_image: "base" + source_suffix: "-main" tag_suffix: "" - flavor: "nvidia" - base_image: "ghcr.io/ublue-os/bluefin-dx-nvidia:stable-daily" + source_image: "base" + source_suffix: "-nvidia" tag_suffix: "-nvidia" steps: @@ -104,9 +107,12 @@ jobs: tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} build-args: | - BASE_IMAGE=${{ matrix.variant.base_image }} + SOURCE_IMAGE=${{ matrix.variant.source_image }} + SOURCE_SUFFIX=${{ matrix.variant.source_suffix }} + FEDORA_VERSION=${{ env.FEDORA_VERSION }} IMAGE_NAME=${{ env.IMAGE_NAME }} IMAGE_VENDOR=${{ github.repository_owner }} + IMAGE_FLAVOR=${{ matrix.variant.flavor }} oci: false - name: Login to GitHub Container Registry diff --git a/.gitignore b/.gitignore index ab799f8..11ff8f6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,13 @@ changelog.md # Titanoboa ISO builder _titanoboa/ +podman_scp.* # VM testing _vm_build/ +.vm/ # ISO files *.iso result-iso/ +dot_files/nvim/lazy-lock.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49eb64b..ed4279d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,8 +37,7 @@ hypercube/ │ ├── nvim/ # Neovim/LazyVim setup │ ├── qt6ct/ # Qt6 theming │ ├── quickshell/ # App launcher -│ ├── starship/ # Shell prompt -│ └── wezterm/ # WezTerm terminal +│ └── starship/ # Shell prompt │ ├── system_files/ # System-level files │ └── shared/ diff --git a/Containerfile b/Containerfile index fa4f098..893dfa3 100644 --- a/Containerfile +++ b/Containerfile @@ -1,12 +1,15 @@ -# Hypercube Container Build -# Aligned with Bluefin patterns +# Hypercube v2 Container Build +# Built from ublue-os/base-main (pure Hyprland, no GNOME) # ============================================ # Build Arguments # ============================================ -ARG BASE_IMAGE=ghcr.io/ublue-os/bluefin-dx:stable-daily +ARG SOURCE_IMAGE="base" +ARG SOURCE_SUFFIX="-main" +ARG FEDORA_VERSION="43" ARG IMAGE_NAME=hypercube ARG IMAGE_VENDOR=binarypie-dev +ARG IMAGE_FLAVOR="main" ARG SHA_HEAD_SHORT="" # ============================================ @@ -20,16 +23,20 @@ COPY build_files /build_files # ============================================ # Stage 2: Main Build # ============================================ -FROM ${BASE_IMAGE} +FROM ghcr.io/ublue-os/${SOURCE_IMAGE}${SOURCE_SUFFIX}:${FEDORA_VERSION} # Re-declare ARGs after FROM (they don't persist across stages) ARG IMAGE_NAME ARG IMAGE_VENDOR +ARG IMAGE_FLAVOR ARG SHA_HEAD_SHORT +ARG FEDORA_VERSION # Export build-time environment variables ENV IMAGE_NAME=${IMAGE_NAME} ENV IMAGE_VENDOR=${IMAGE_VENDOR} +ENV IMAGE_FLAVOR=${IMAGE_FLAVOR} +ENV FEDORA_VERSION=${FEDORA_VERSION} # Copy dot_files (config templates) into the image COPY dot_files /usr/share/hypercube/config diff --git a/Justfile b/Justfile index a3e48de..4015d37 100644 --- a/Justfile +++ b/Justfile @@ -1,11 +1,10 @@ # Hypercube Build System -# Aligned with Bluefin patterns for consistency # Configuration export repo_organization := env("REPO_ORGANIZATION", "binarypie-dev") export image_name := env("IMAGE_NAME", "hypercube") -export base_image := env("BASE_IMAGE", "ghcr.io/ublue-os/bluefin-dx") -export base_image_nvidia := env("BASE_IMAGE_NVIDIA", "ghcr.io/ublue-os/bluefin-dx-nvidia") +export base_image := env("BASE_IMAGE", "ghcr.io/ublue-os/base-main") +export base_image_nvidia := env("BASE_IMAGE_NVIDIA", "ghcr.io/ublue-os/base-nvidia") export default_tag := env("DEFAULT_TAG", "stable-daily") # Runtime detection @@ -102,6 +101,85 @@ run flavor="main": "localhost/{{ image_name }}:${TAG}" \ /bin/bash +# ============================================ +# VM Recipes +# ============================================ + +# Run ISO in local VM with virt-manager (persistent disk for testing installs) +[group('VM')] +run-iso-local iso_file: + #!/usr/bin/bash + set -euo pipefail + + if [[ ! -f "{{ iso_file }}" ]]; then + echo "Error: ISO file not found: {{ iso_file }}" + exit 1 + fi + + ISO_PATH="$(realpath {{ iso_file }})" + VM_NAME="hypercube-test" + # Store disk in current directory to avoid libvirt permission issues + DISK_PATH="$(pwd)/.vm/${VM_NAME}.qcow2" + + # RAM: 16GB for ostreecontainer deployment (needs temp space in tmpfs) + # vCPUs: 8 cores for faster installation + ram_size=16 + vcpus=8 + + echo "VM: ${VM_NAME}" + echo "RAM: ${ram_size}GB, vCPUs: ${vcpus}" + echo "Disk: ${DISK_PATH}" + + # Check if VM already exists + if virsh dominfo "${VM_NAME}" &>/dev/null; then + echo "VM '${VM_NAME}' already exists." + echo "Starting existing VM (will boot from disk, not ISO)..." + virsh start "${VM_NAME}" || true + virt-manager --connect qemu:///system --show-domain-console "${VM_NAME}" + exit 0 + fi + + # Create disk directory and disk if needed + mkdir -p "$(dirname "${DISK_PATH}")" + if [[ ! -f "${DISK_PATH}" ]]; then + echo "Creating disk: ${DISK_PATH}" + qemu-img create -f qcow2 "${DISK_PATH}" 64G + fi + + echo "Creating new VM with ISO..." + echo "After installation, run 'just run-iso-local ' again to boot from disk" + + # Create persistent VM + virt-install \ + --name "${VM_NAME}" \ + --memory $(( ram_size * 1024 )) \ + --vcpus ${vcpus} \ + --cdrom "${ISO_PATH}" \ + --disk path="${DISK_PATH}",format=qcow2,bus=virtio \ + --os-variant fedora-unknown \ + --boot uefi \ + --autoconsole graphical + +# Delete the test VM and its disk +[group('VM')] +delete-test-vm: + #!/usr/bin/bash + set -euo pipefail + VM_NAME="hypercube-test" + DISK_PATH="$(pwd)/.vm/${VM_NAME}.qcow2" + + echo "Stopping VM if running..." + virsh destroy "${VM_NAME}" 2>/dev/null || true + + echo "Removing VM definition..." + virsh undefine "${VM_NAME}" --nvram 2>/dev/null || true + + echo "Removing disk..." + rm -f "${DISK_PATH}" + rmdir "$(pwd)/.vm" 2>/dev/null || true + + echo "Test VM deleted." + # ============================================ # ISO Recipes # ============================================ @@ -117,18 +195,23 @@ _titanoboa-setup: git -C _titanoboa pull --ff-only || true else echo "Cloning Titanoboa..." - git clone --depth 1 "https://github.com/ublue-os/titanoboa.git" _titanoboa + git clone --depth 1 "https://github.com/binarypie-dev/titanoboa.git" _titanoboa fi - # Patch Titanoboa to use --policy=missing for local image builds - sed -i 's/PODMAN }} pull /PODMAN }} pull --policy=missing /' _titanoboa/Justfile - sed -i 's/podman pull /podman pull --policy=missing /' _titanoboa/Justfile + # Patch Titanoboa for local builds: + # - Add --policy=missing to avoid re-pulling existing images + # - Add --tls-verify=false for insecure local registries + sed -i 's/podman pull /podman pull --tls-verify=false --policy=missing /' _titanoboa/Justfile + sed -i 's/PODMAN }} pull /PODMAN }} pull --tls-verify=false --policy=missing /' _titanoboa/Justfile -# Build ISO from local image +# Build ISO from local image using a temporary local registry [group('ISO')] -build-iso flavor="main": _titanoboa-setup +build-iso-local flavor="main": _titanoboa-setup #!/usr/bin/bash set -euo pipefail + REGISTRY_PORT=5000 + REGISTRY_NAME="hypercube-registry" + if [[ "{{ flavor }}" == "nvidia" ]]; then TAG="{{ default_tag }}-nvidia" ISO_NAME="{{ image_name }}-nvidia.iso" @@ -137,37 +220,99 @@ build-iso flavor="main": _titanoboa-setup ISO_NAME="{{ image_name }}.iso" fi - IMAGE_FULL="localhost/{{ image_name }}:${TAG}" + LOCAL_IMAGE="localhost/{{ image_name }}:${TAG}" - echo "Building ISO for ${IMAGE_FULL}..." + # Get the host IP address that the chroot can reach + # Use the default route interface IP, or fall back to hostname -I + HOST_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || hostname -I | awk '{print $1}') + if [[ -z "$HOST_IP" ]]; then + echo "Error: Could not determine host IP address" + exit 1 + fi + REGISTRY_IMAGE="${HOST_IP}:${REGISTRY_PORT}/{{ image_name }}:${TAG}" - # Check if image exists - ID=$({{ PODMAN }} images --filter reference="${IMAGE_FULL}" --format "{{{{.ID}}}}") + echo "Building ISO for ${LOCAL_IMAGE}..." + echo "Using registry at ${HOST_IP}:${REGISTRY_PORT}" + + # Check if image exists in user storage + ID=$({{ PODMAN }} images --filter reference="${LOCAL_IMAGE}" --format "{{{{.ID}}}}") if [[ -z "$ID" ]]; then - echo "Error: Image ${IMAGE_FULL} not found. Run 'just build {{ flavor }}' first." + echo "Error: Image ${LOCAL_IMAGE} not found. Run 'just build {{ flavor }}' first." exit 1 fi + # Cleanup function + cleanup() { + echo "Stopping local registry..." + {{ SUDO }} {{ PODMAN }} stop "${REGISTRY_NAME}" 2>/dev/null || true + {{ SUDO }} {{ PODMAN }} rm "${REGISTRY_NAME}" 2>/dev/null || true + } + trap cleanup EXIT + + # Stop any existing registry container from a previous failed build + {{ SUDO }} {{ PODMAN }} stop "${REGISTRY_NAME}" 2>/dev/null || true + {{ SUDO }} {{ PODMAN }} rm "${REGISTRY_NAME}" 2>/dev/null || true + # Copy image to root podman storage if running as non-root if [[ "${UID}" -gt 0 ]]; then echo "Copying image to root podman storage..." COPYTMP=$(mktemp -p "${PWD}" -d -t podman_scp.XXXXXXXXXX) - {{ SUDO }} TMPDIR=${COPYTMP} {{ PODMAN }} image scp "${UID}@localhost::${IMAGE_FULL}" root@localhost::"${IMAGE_FULL}" + {{ SUDO }} TMPDIR=${COPYTMP} {{ PODMAN }} image scp "${UID}@localhost::${LOCAL_IMAGE}" root@localhost::"${LOCAL_IMAGE}" rm -rf "${COPYTMP}" fi + # Start local registry bound to all interfaces + echo "Starting local registry on port ${REGISTRY_PORT}..." + {{ SUDO }} {{ PODMAN }} run -d --rm \ + --name "${REGISTRY_NAME}" \ + -p 0.0.0.0:${REGISTRY_PORT}:5000 \ + docker.io/library/registry:2 + + # Wait for registry to be ready + echo "Waiting for registry to be ready..." + for i in {1..30}; do + if curl -s "http://${HOST_IP}:${REGISTRY_PORT}/v2/" > /dev/null 2>&1; then + echo "Registry is ready" + break + fi + sleep 1 + done + + # Tag and push image to local registry + echo "Pushing image to local registry..." + {{ SUDO }} {{ PODMAN }} tag "${LOCAL_IMAGE}" "${REGISTRY_IMAGE}" + {{ SUDO }} {{ PODMAN }} push --tls-verify=false "${REGISTRY_IMAGE}" + + # Save project root for later + PROJECT_ROOT="${PWD}" + + # Copy iso_files to _titanoboa so they're available at /app/iso_files inside the chroot + echo "Copying iso_files to _titanoboa..." + rm -rf _titanoboa/iso_files + cp -r iso_files _titanoboa/iso_files + # Build ISO with Titanoboa + # livesys=1 enables livesys-scripts from binarypie/hypercube COPR (includes Hyprland support) + # HOOK_post_rootfs installs Anaconda and copies live ISO configs + # Parameters (positional): image livesys flatpaks_file compression extra_kargs container_image polkit livesys_repo cd _titanoboa - {{ SUDO }} just build "${IMAGE_FULL}" - - # Fix ownership - if [[ "${UID}" -gt 0 ]]; then - {{ SUDO }} chown "${UID}:$(id -g)" -R "${PWD}" - fi + {{ SUDO }} HOOK_post_rootfs="${PROJECT_ROOT}/iso_files/hook-post-rootfs.sh" just build \ + "${REGISTRY_IMAGE}" \ + 1 \ + "${PROJECT_ROOT}/flatpaks/system-flatpaks.list" \ + squashfs \ + NONE \ + "${REGISTRY_IMAGE}" \ + 1 \ + binarypie/hypercube + + # Stop registry before moving ISO (while sudo is still cached) + cleanup + trap - EXIT # Move ISO to project root if [[ -f "output.iso" ]]; then - mv output.iso "../${ISO_NAME}" + mv output.iso "${PROJECT_ROOT}/${ISO_NAME}" echo "" echo "========================================" echo "ISO built successfully: ${ISO_NAME}" @@ -197,8 +342,27 @@ build-iso-ghcr flavor="main": _titanoboa-setup echo "Building ISO for ${IMAGE_FULL}..." + # Save project root for later + PROJECT_ROOT="${PWD}" + + # Copy iso_files to _titanoboa so they're available at /app/iso_files inside the chroot + echo "Copying iso_files to _titanoboa..." + rm -rf _titanoboa/iso_files + cp -r iso_files _titanoboa/iso_files + + # livesys=1 enables livesys-scripts from binarypie/hypercube COPR (includes Hyprland support) + # HOOK_post_rootfs installs Anaconda and copies live ISO configs + # Parameters (positional): image livesys flatpaks_file compression extra_kargs container_image polkit livesys_repo cd _titanoboa - {{ SUDO }} just build "${IMAGE_FULL}" + {{ SUDO }} HOOK_post_rootfs="${PROJECT_ROOT}/iso_files/hook-post-rootfs.sh" just build \ + "${IMAGE_FULL}" \ + 1 \ + "${PROJECT_ROOT}/flatpaks/system-flatpaks.list" \ + squashfs \ + NONE \ + "${IMAGE_FULL}" \ + 1 \ + binarypie/hypercube if [[ "${UID}" -gt 0 ]]; then {{ SUDO }} chown "${UID}:$(id -g)" -R "${PWD}" diff --git a/README.md b/README.md index 5dabc9b..f3b1514 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Pre-configured and ready to use: - **Neovim** (nightly) with LazyVim, LSP, and language support - **Lazygit** for interactive Git operations - **Fish** shell with Starship prompt -- **Ghostty** & **WezTerm** GPU-accelerated terminals +- **Ghostty** GPU-accelerated terminal - **Quickshell** application launcher and system controls ### Consistent Theming @@ -99,7 +99,7 @@ On top of Bluefin-DX, Hypercube adds: | Category | Packages | |----------|----------| | Compositor | Hyprland, Hyprlock, Hypridle, Hyprpaper, Hyprshot | -| Terminals | Ghostty, WezTerm | +| Terminals | Ghostty | | Editor | Neovim (nightly) | | Git Tools | Lazygit | | Launcher | Quickshell | @@ -112,7 +112,7 @@ All configurations live in `/usr/share/hypercube/config/` and can be overridden - Fish shell with vim mode and Starship prompt - Hyprland with vim-style navigation - Neovim with LazyVim distribution -- Ghostty and WezTerm with Tokyo Night colors +- Ghostty with Tokyo Night colors - GTK/Qt theming with dark mode ## Documentation diff --git a/build_files/base/01-base-system.sh b/build_files/base/01-base-system.sh new file mode 100755 index 0000000..d26e77b --- /dev/null +++ b/build_files/base/01-base-system.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Hypercube Base System +# Installs core system components, display manager (greetd + regreet), and hardware support + +set -ouex pipefail + +echo "Installing Hypercube base system..." + +# Clean DNF cache first +dnf5 -y clean all + +### Display Manager: greetd + tuigreet +# Both are in official Fedora repos +# Note: Can switch to regreet (GTK greeter) when F43 COPR is available +dnf5 -y install \ + greetd \ + greetd-selinux \ + tuigreet + +### Desktop Portals & Integration +dnf5 -y install \ + xdg-desktop-portal \ + xdg-desktop-portal-gtk \ + xdg-user-dirs \ + xdg-utils + +### Credential & Secret Storage +dnf5 -y install \ + gnome-keyring \ + seahorse + +### Audio (Pipewire should be in base, but ensure full stack) +dnf5 -y install \ + pipewire \ + pipewire-pulseaudio \ + pipewire-alsa \ + wireplumber + +### Networking +dnf5 -y install \ + NetworkManager \ + NetworkManager-wifi \ + NetworkManager-bluetooth \ + network-manager-applet + +### Bluetooth +dnf5 -y install \ + bluez \ + bluez-tools + +### Power Management +dnf5 -y install \ + power-profiles-daemon \ + upower + +### Fonts (base set - more can be added) +dnf5 -y install \ + google-noto-fonts-common \ + google-noto-sans-fonts \ + google-noto-serif-fonts \ + google-noto-sans-mono-fonts \ + google-noto-emoji-fonts \ + fontawesome-fonts-all \ + jetbrains-mono-fonts-all + +### Utilities +dnf5 -y install \ + wl-clipboard \ + xdg-utils \ + polkit \ + dbus-daemon + +### File Management +dnf5 -y install \ + nautilus \ + file-roller \ + gvfs \ + gvfs-mtp \ + gvfs-gphoto2 \ + gvfs-smb + +### Create greeter user for greetd +# greetd runs the greeter as this user +if ! id -u greeter &>/dev/null; then + useradd -r -M -s /usr/bin/nologin greeter +fi + +### Enable services +systemctl enable greetd.service +systemctl enable NetworkManager.service +systemctl enable bluetooth.service +systemctl enable power-profiles-daemon.service +systemctl enable hypercube-first-boot.service + +### Disable services we don't need +systemctl disable gdm.service 2>/dev/null || true +systemctl disable sddm.service 2>/dev/null || true + +echo "Hypercube base system installed successfully" diff --git a/build_files/dx/01-dx-tooling.sh b/build_files/dx/01-dx-tooling.sh new file mode 100755 index 0000000..b619c75 --- /dev/null +++ b/build_files/dx/01-dx-tooling.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Hypercube DX (Developer Experience) Tooling +# Installs Distrobox, container tools, and dev utilities + +set -ouex pipefail + +echo "Installing DX tooling..." + +### Distrobox - Container-based dev environments +dnf5 -y install distrobox + +### Container tools (Podman is in base-main, add full stack) +dnf5 -y install \ + podman \ + podman-compose \ + buildah \ + skopeo + +### Podman Desktop - GUI for containers and Kubernetes +# Installed via Flatpak for automatic updates +flatpak install -y flathub io.podman_desktop.PodmanDesktop + +### Additional dev utilities +dnf5 -y install \ + just \ + direnv \ + openssl \ + openssh-clients \ + rsync \ + wget \ + curl \ + unzip \ + zip \ + tar + +echo "DX tooling installed successfully" diff --git a/build_files/hypercube/00-hypercube-branding.sh b/build_files/hypercube/01-hypercube-branding.sh similarity index 86% rename from build_files/hypercube/00-hypercube-branding.sh rename to build_files/hypercube/01-hypercube-branding.sh index 4162d8d..1360ad0 100755 --- a/build_files/hypercube/00-hypercube-branding.sh +++ b/build_files/hypercube/01-hypercube-branding.sh @@ -9,7 +9,7 @@ IMAGE_NAME="${IMAGE_NAME:-hypercube}" IMAGE_VENDOR="${IMAGE_VENDOR:-binarypie-dev}" IMAGE_FLAVOR="${IMAGE_FLAVOR:-main}" FEDORA_VERSION="${FEDORA_VERSION:-$(rpm -E %fedora)}" -BASE_IMAGE_NAME="${BASE_IMAGE_NAME:-bluefin-dx}" +BASE_IMAGE_NAME="${BASE_IMAGE_NAME:-base-main}" # Create image-info.json mkdir -p /usr/share/hypercube @@ -26,22 +26,22 @@ cat > /usr/share/hypercube/image-info.json << EOF EOF # Modify /usr/lib/os-release for Hypercube branding -# Keep Bluefin as ID_LIKE since we inherit from it +# No longer inheriting from Bluefin - built from base-main if [ -f /usr/lib/os-release ]; then # Set NAME to Hypercube sed -i "s/^NAME=.*/NAME=\"Hypercube\"/" /usr/lib/os-release # Update PRETTY_NAME - sed -i "s/^PRETTY_NAME=.*/PRETTY_NAME=\"Hypercube ${FEDORA_VERSION} (Fedora-based)\"/" /usr/lib/os-release + sed -i "s/^PRETTY_NAME=.*/PRETTY_NAME=\"Hypercube ${FEDORA_VERSION}\"/" /usr/lib/os-release - # Set ID to hypercube, keep fedora and bluefin as ID_LIKE + # Set ID to hypercube, keep fedora as ID_LIKE sed -i "s/^ID=.*/ID=hypercube/" /usr/lib/os-release - # Update ID_LIKE to include both fedora and bluefin + # Update ID_LIKE to fedora (no longer inheriting from Bluefin) if grep -q "^ID_LIKE=" /usr/lib/os-release; then - sed -i "s/^ID_LIKE=.*/ID_LIKE=\"bluefin fedora\"/" /usr/lib/os-release + sed -i "s/^ID_LIKE=.*/ID_LIKE=\"fedora\"/" /usr/lib/os-release else - echo 'ID_LIKE="bluefin fedora"' >> /usr/lib/os-release + echo 'ID_LIKE="fedora"' >> /usr/lib/os-release fi # Set VARIANT_ID @@ -100,11 +100,10 @@ if [ -d /usr/share/plymouth/themes/hypercube ]; then echo "Plymouth theme set to hypercube" fi -### GDM Login Screen Branding -# Compile dconf database for GDM logo -if [ -d /etc/dconf/db/gdm.d ]; then +### dconf database update (for any remaining dconf settings) +if [ -d /etc/dconf/db ]; then dconf update - echo "GDM dconf database updated" + echo "dconf database updated" fi echo "Hypercube branding applied successfully" diff --git a/build_files/hypercube/01-hypercube-packages.sh b/build_files/hypercube/01-hypercube-packages.sh deleted file mode 100755 index 9ce7931..0000000 --- a/build_files/hypercube/01-hypercube-packages.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# Hypercube Package Installation -# Installs Hyprland compositor stack, CLI tools, terminals, and editors - -set -ouex pipefail - -echo "Installing Hypercube packages..." - -# Clean DNF cache first -dnf5 -y clean all - -### Compositor / Hyprland Stack -dnf5 -y copr enable sdegler/hyprland -dnf5 -y install \ - hyprland \ - hyprland-contrib \ - hyprland-plugins \ - hyprland-qtutils \ - hyprpaper \ - hyprpicker \ - hypridle \ - hyprshot \ - hyprlock \ - hyprpolkitagent \ - pyprland \ - waybar-git \ - xdg-desktop-portal-hyprland - -### CLI Tools (skip packages already in bluefin-dx) -# Note: starship is already installed in bluefin-dx -dnf5 -y install \ - fd-find \ - qt6ct - -### Lazygit from COPR -dnf5 -y copr enable atim/lazygit -dnf5 -y install lazygit - -### Quickshell - Application Launcher, Notifications, OSD -dnf5 -y copr enable errornointernet/quickshell -dnf5 -y install quickshell - -### Terminals -dnf5 -y copr enable wezfurlong/wezterm-nightly -dnf5 -y install wezterm - -dnf5 -y copr enable scottames/ghostty -dnf5 -y install ghostty - -### Editor -dnf5 -y copr enable agriffis/neovim-nightly -dnf5 -y install neovim python3-neovim - -echo "Hypercube packages installed successfully" diff --git a/build_files/hypercube/03-hypercube-configs.sh b/build_files/hypercube/03-hypercube-configs.sh index 28e810a..71888c7 100755 --- a/build_files/hypercube/03-hypercube-configs.sh +++ b/build_files/hypercube/03-hypercube-configs.sh @@ -42,20 +42,20 @@ config-file = /usr/share/hypercube/config/ghostty/config # Your customizations below: EOF -### Wezterm terminal - stub that sources system config -# Users can customize by modifying the config table after dofile() -mkdir -p /etc/skel/.config/wezterm -cat > /etc/skel/.config/wezterm/wezterm.lua << 'EOF' --- Hypercube Wezterm Configuration --- System defaults are loaded below. Add your customizations after this line. --- To replace defaults entirely, remove the dofile line and start fresh. +### Hyprland - stub that sources system config +# Users can customize by adding settings after the source line +# Hyprland parses linearly: system config first, then user customizations +mkdir -p /etc/skel/.config/hypr +cat > /etc/skel/.config/hypr/hyprland.conf << 'EOF' +# Hypercube Hyprland Configuration +# System defaults are sourced below. Add your customizations after this line. +# Settings defined after source will override the defaults. +# To replace defaults entirely, remove or comment out the source line. -local config = dofile("/usr/share/hypercube/config/wezterm/wezterm.lua") +source = /usr/share/hypercube/config/hypr/hyprland.conf --- Your customizations below: --- Example: config.font_size = 14 +# Your customizations below: -return config EOF ### GTK theme settings - install to /etc/xdg/ for system-wide defaults diff --git a/build_files/hypercube/99-tests.sh b/build_files/hypercube/99-tests.sh index 0cf354b..ab09139 100755 --- a/build_files/hypercube/99-tests.sh +++ b/build_files/hypercube/99-tests.sh @@ -8,51 +8,83 @@ echo "Running Hypercube validation tests..." ### Check critical packages are installed REQUIRED_PACKAGES=( - "hyprland" - "hyprlock" - "hypridle" - "quickshell" - "wezterm" - "ghostty" - "neovim" - "lazygit" + # Display manager + "greetd" + "tuigreet" + # Hyprland stack + "hyprland" + "hyprlock" + "hypridle" + "quickshell" + # Terminals + "ghostty" + # Dev tools + "neovim" + "lazygit" + "fish" + "starship" + # DX tooling + "distrobox" + "podman" ) for pkg in "${REQUIRED_PACKAGES[@]}"; do - if ! rpm -q "$pkg" &>/dev/null; then - echo "ERROR: Required package '$pkg' is not installed!" - exit 1 - fi - echo " OK: $pkg installed" + if ! rpm -q "$pkg" &>/dev/null; then + echo "ERROR: Required package '$pkg' is not installed!" + exit 1 + fi + echo " OK: $pkg installed" done ### Check critical files exist REQUIRED_FILES=( - "/usr/share/hypercube/image-info.json" - "/usr/lib/environment.d/60-hypercube-xdg.conf" - "/usr/bin/nvimd" - "/etc/fish/config.fish" - "/usr/share/hypercube/config/starship/starship.toml" - "/usr/share/themes/Tokyonight-Dark/gtk-3.0/gtk.css" - "/usr/share/icons/Tokyonight-Dark/index.theme" - "/usr/share/plymouth/themes/hypercube/hypercube.plymouth" - "/usr/share/pixmaps/hypercube-logo.png" - "/usr/share/backgrounds/hypercube/background.png" + # Branding + "/usr/share/hypercube/image-info.json" + "/usr/lib/environment.d/60-hypercube-xdg.conf" + # greetd + "/etc/greetd/config.toml" + # Config files + "/usr/bin/nvimd" + "/etc/fish/config.fish" + "/usr/share/hypercube/config/starship/starship.toml" + # Theming + "/usr/share/themes/Tokyonight-Dark/gtk-3.0/gtk.css" + "/usr/share/icons/Tokyonight-Dark/index.theme" + "/usr/share/plymouth/themes/hypercube/hypercube.plymouth" + "/usr/share/pixmaps/hypercube-logo.png" + "/usr/share/backgrounds/hypercube/background.png" + # DX config + "/etc/distrobox/distrobox.ini" + "/usr/share/ublue-os/just/61-dx.just" ) for file in "${REQUIRED_FILES[@]}"; do - if [ ! -e "$file" ]; then - echo "ERROR: Required file '$file' does not exist!" - exit 1 - fi - echo " OK: $file exists" + if [ ! -e "$file" ]; then + echo "ERROR: Required file '$file' does not exist!" + exit 1 + fi + echo " OK: $file exists" done ### Check os-release branding if ! grep -q "ID=hypercube" /usr/lib/os-release; then - echo "ERROR: os-release branding not applied!" - exit 1 + echo "ERROR: os-release branding not applied!" + exit 1 fi echo " OK: os-release branding applied" +### Check services are enabled +REQUIRED_SERVICES=( + "greetd.service" + "NetworkManager.service" +) + +for svc in "${REQUIRED_SERVICES[@]}"; do + if ! systemctl is-enabled "$svc" &>/dev/null; then + echo "ERROR: Required service '$svc' is not enabled!" + exit 1 + fi + echo " OK: $svc enabled" +done + echo "All Hypercube validation tests passed!" diff --git a/build_files/hyprland/01-hyprland-desktop.sh b/build_files/hyprland/01-hyprland-desktop.sh new file mode 100755 index 0000000..eb2f5e3 --- /dev/null +++ b/build_files/hyprland/01-hyprland-desktop.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Hypercube Hyprland Desktop Stack +# Installs Hyprland compositor, tools, terminals, and editors + +set -ouex pipefail + +echo "Installing Hyprland desktop stack..." + +### Enable Hypercube COPR (our self-maintained packages) +dnf5 -y copr enable binarypie/hypercube + +### Compositor / Hyprland Stack (from Hypercube COPR) +dnf5 -y install \ + hyprland \ + hyprland-guiutils \ + hyprland-uwsm \ + hyprpaper \ + hypridle \ + hyprlock \ + hyprpolkitagent \ + xdg-desktop-portal-hyprland + +### Quickshell - Application Launcher, Notifications, OSD (from Hypercube COPR) +dnf5 -y install quickshell + +### CLI Tools - Official Fedora repos +dnf5 -y install \ + fd-find \ + fzf \ + ripgrep \ + bat \ + zoxide \ + htop \ + btop \ + git \ + jq \ + yq \ + tmux \ + qt6ct + +### CLI Tools - From Hypercube COPR +dnf5 -y install \ + eza \ + starship \ + lazygit + +### Fish Shell (set as default) +dnf5 -y install fish + +### Terminal - Ghostty (from scottames COPR) +dnf5 -y copr enable scottames/ghostty +dnf5 -y install ghostty + +### Editor - Neovim nightly (from agriffis COPR) +dnf5 -y copr enable agriffis/neovim-nightly +dnf5 -y install neovim python3-neovim + +### Image/Media Viewers +dnf5 -y install \ + imv \ + mpv + +### Screenshot/Screen Recording +dnf5 -y install \ + grim \ + slurp + +echo "Hyprland desktop stack installed successfully" diff --git a/build_files/shared/build.sh b/build_files/shared/build.sh index 9cb8e61..a70a0f4 100755 --- a/build_files/shared/build.sh +++ b/build_files/shared/build.sh @@ -1,11 +1,11 @@ #!/bin/bash -# Hypercube Build Orchestrator +# Hypercube v2 Build Orchestrator # Main build script that coordinates all build steps set -ouex pipefail echo "========================================" -echo "Starting Hypercube Build" +echo "Starting Hypercube v2 Build" echo "========================================" ### Rsync system files to root filesystem @@ -13,15 +13,30 @@ echo "Installing system files..." # Note: We don't use --ignore-existing so our files override base image files rsync -rlpvh /ctx/system_files/shared/ / -### Run hypercube build scripts in order -echo "Running build scripts..." -for script in /ctx/build_files/hypercube/*.sh; do - if [[ -f "$script" && -x "$script" ]]; then - echo "" - echo "========================================" - echo "Running: $(basename "$script")" - echo "========================================" - "$script" +### Run build scripts in order from each phase directory +# Phase 1: Base system (greetd, portals, hardware) +# Phase 2: Hyprland desktop +# Phase 3: DX tooling +# Phase 4: Hypercube theming/branding + +BUILD_DIRS=( + "/ctx/build_files/base" + "/ctx/build_files/hyprland" + "/ctx/build_files/dx" + "/ctx/build_files/hypercube" +) + +for build_dir in "${BUILD_DIRS[@]}"; do + if [[ -d "$build_dir" ]]; then + for script in "$build_dir"/*.sh; do + if [[ -f "$script" && -x "$script" ]]; then + echo "" + echo "========================================" + echo "Running: $(basename "$build_dir")/$(basename "$script")" + echo "========================================" + "$script" + fi + done fi done @@ -34,5 +49,5 @@ echo "========================================" echo "" echo "========================================" -echo "Hypercube Build Complete!" +echo "Hypercube v2 Build Complete!" echo "========================================" diff --git a/build_files/shared/clean-stage.sh b/build_files/shared/clean-stage.sh index 65ce81d..1a170b2 100755 --- a/build_files/shared/clean-stage.sh +++ b/build_files/shared/clean-stage.sh @@ -8,12 +8,9 @@ echo "Disabling COPR repositories..." # Disable all COPR repos that were enabled during build # This ensures the final image doesn't have external repos enabled -dnf5 -y copr disable sdegler/hyprland || true -dnf5 -y copr disable errornointernet/quickshell || true -dnf5 -y copr disable wezfurlong/wezterm-nightly || true -dnf5 -y copr disable scottames/ghostty || true -dnf5 -y copr disable agriffis/neovim-nightly || true -dnf5 -y copr disable atim/lazygit || true + +# Hypercube COPR (our self-maintained packages) +dnf5 -y copr disable binarypie/hypercube || true echo "Cleaning package caches..." dnf5 -y clean all diff --git a/dot_files/fish/conf.d/rustup.fish b/dot_files/fish/conf.d/rustup.fish index e4cb363..4b676f9 100644 --- a/dot_files/fish/conf.d/rustup.fish +++ b/dot_files/fish/conf.d/rustup.fish @@ -1 +1,2 @@ -source "$HOME/.cargo/env.fish" +# Only source cargo env if rustup is installed +test -f "$HOME/.cargo/env.fish" && source "$HOME/.cargo/env.fish" diff --git a/dot_files/fish/config.fish b/dot_files/fish/config.fish index f68dab3..e0f9bb1 100644 --- a/dot_files/fish/config.fish +++ b/dot_files/fish/config.fish @@ -21,8 +21,6 @@ if status is-interactive fish_add_path $HOME/Code/flutter/bin fish_add_path $HOME/go/bin - # Alias - alias wezterm 'flatpak run org.wezfurlong.wezterm' end ### bling.fish source start diff --git a/dot_files/ghostty/config b/dot_files/ghostty/config index f420681..317b638 100644 --- a/dot_files/ghostty/config +++ b/dot_files/ghostty/config @@ -1,4 +1,4 @@ -# Ghostty keybindings matching WezTerm config +# Ghostty keybindings with tmux-style leader key # Leader key is CTRL+a (simulated via prefix since Linux doesn't support global leader) # ---------------------------------------------------------------- @@ -96,8 +96,7 @@ keybind = ctrl+a>w=toggle_tab_overview # Create a new tab keybind = ctrl+a>c=new_tab -# Rename current tab (Ghostty doesn't support this natively, but we can use new_tab as closest) -# Note: ctrl+a>, is for rename in WezTerm but Ghostty doesn't have direct rename support +# Rename current tab # Move to next/previous tab keybind = ctrl+a>n=next_tab @@ -142,8 +141,7 @@ keybind = ctrl+a>l=goto_split:right keybind = ctrl+a>k=goto_split:top keybind = ctrl+a>j=goto_split:bottom -# Pane resizing (ctrl+a>r enters resize mode, then h/j/k/l to resize) -# Note: Ghostty doesn't have key tables/modes like WezTerm, so we use direct bindings +# Pane resizing (ctrl+a>r then h/j/k/l to resize) keybind = ctrl+a>r>h=resize_split:left,10 keybind = ctrl+a>r>l=resize_split:right,10 keybind = ctrl+a>r>k=resize_split:up,10 diff --git a/dot_files/hypr/hyprland.conf b/dot_files/hypr/hyprland.conf index 898576c..f444da0 100644 --- a/dot_files/hypr/hyprland.conf +++ b/dot_files/hypr/hyprland.conf @@ -17,7 +17,8 @@ # - Vim-like navigation: SUPER + h/j/k/l (left/down/up/right) # - Move windows: SUPER + SHIFT + h/j/k/l # - Resize windows: SUPER + CTRL + h/j/k/l -# - App launcher: SUPER + R (launches Quickshell launcher) +# - App launcher: SUPER + R (opens left sidebar with search) +# - Quick settings: SUPER + N (opens right sidebar) # # ####################################################################################### @@ -42,7 +43,7 @@ monitor=,preferred,auto,auto # Set programs that you use $terminal = ghostty $fileManager = files -$menu = quickshell msg launcher toggle +$menu = qs ipc call shell toggleSidebarLeft ################# ### AUTOSTART ### @@ -214,8 +215,7 @@ input { kb_variant = kb_model = # caps:ctrl_modifier - Caps Lock acts as Ctrl - # altwin:swap_alt_win - Swap Alt and Super (Win) keys - kb_options = caps:ctrl_modifier,altwin:swap_alt_win + kb_options = caps:ctrl_modifier kb_rules = follow_mouse = 1 sensitivity = 0 # -1.0 - 1.0, 0 means no modification. @@ -249,9 +249,12 @@ bind = $mainMod, E, exec, $fileManager bind = $mainMod, V, togglefloating, bind = $mainMod, F, fullscreen, 0 # Fullscreen bind = $mainMod, R, exec, $menu +# bind = $mainMod, A, exec, qs ipc call shell toggleSidebarLeft # Removed - use SUPER+R instead +bind = $mainMod, N, exec, qs ipc call shell toggleSidebarRight # Notifications/quick settings +bind = $mainMod, Escape, exec, qs ipc call shell closeAll # Close all panels bind = $mainMod, P, pseudo, # dwindle bind = $mainMod, S, togglesplit, # dwindle -bind = $mainMod SHIFT, L, exec, hyprlock # Lock screen +bind = $mainMod CTRL, Escape, exec, hyprlock # Lock screen # Move focus with mainMod + vim keys (h/j/k/l) bind = $mainMod, H, movefocus, l @@ -313,13 +316,16 @@ bind = $mainMod, mouse_up, workspace, e-1 bindm = $mainMod, mouse:272, movewindow bindm = $mainMod, mouse:273, resizewindow -# Laptop multimedia keys for volume and LCD brightness (using Quickshell OSD) -bindel = ,XF86AudioRaiseVolume, exec, quickshell msg osd volumeUp -bindel = ,XF86AudioLowerVolume, exec, quickshell msg osd volumeDown -bindel = ,XF86AudioMute, exec, quickshell msg osd volumeMute -bindel = ,XF86AudioMicMute, exec, quickshell msg osd micMute -bindel = ,XF86MonBrightnessUp, exec, quickshell msg osd brightnessUp -bindel = ,XF86MonBrightnessDown, exec, quickshell msg osd brightnessDown +# Laptop multimedia keys for volume and LCD brightness +# Volume control with OSD +bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ && qs ipc call shell showOsdVolume "$(wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '{print $2}')" +bindel = ,XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- && qs ipc call shell showOsdVolume "$(wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '{print $2}')" +bindel = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle && qs ipc call shell showOsdVolume "$(wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '{print $2}')" +bindel = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle && qs ipc call shell showOsdMic "$(wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED && echo true || echo false)" + +# Brightness control with OSD +bindel = ,XF86MonBrightnessUp, exec, brightnessctl set 5%+ && qs ipc call shell showOsdBrightness "$(brightnessctl -m | cut -d, -f4 | tr -d '%' | awk '{print $1/100}')" +bindel = ,XF86MonBrightnessDown, exec, brightnessctl set 5%- && qs ipc call shell showOsdBrightness "$(brightnessctl -m | cut -d, -f4 | tr -d '%' | awk '{print $1/100}')" # Requires playerctl bindl = , XF86AudioNext, exec, playerctl next @@ -327,6 +333,13 @@ bindl = , XF86AudioPause, exec, playerctl play-pause bindl = , XF86AudioPlay, exec, playerctl play-pause bindl = , XF86AudioPrev, exec, playerctl previous +# Screenshot with Gradia (Print Screen key) +bind = , Print, exec, flatpak run be.alexandervanhee.gradia --screenshot=INTERACTIVE + +# App Switcher (Super+Tab to switch between windows across all workspaces) +bind = $mainMod, Tab, exec, qs ipc call shell nextWindow +bind = $mainMod SHIFT, Tab, exec, qs ipc call shell prevWindow + ############################## ### WINDOWS AND WORKSPACES ### ############################## @@ -347,3 +360,6 @@ windowrule = nofocus,class:^$,title:^$,xwayland:1,floating:1,fullscreen:0,pinned layerrule = blur,quickshell layerrule = ignorealpha 0,quickshell layerrule = animation slide,quickshell + +# App switcher layer rules +layerrule = animation fade,appswitcher diff --git a/dot_files/nvim/README.md b/dot_files/nvim/README.md index f3e528c..625556b 100644 --- a/dot_files/nvim/README.md +++ b/dot_files/nvim/README.md @@ -84,7 +84,7 @@ The container mounts your local directories: ### Clipboard Support -The container uses OSC 52 for clipboard integration, which works with modern terminals like Ghostty, Kitty, WezTerm, and others that support OSC 52 escape sequences. +The container uses OSC 52 for clipboard integration, which works with modern terminals like Ghostty, Kitty, and others that support OSC 52 escape sequences. ## Plugins diff --git a/dot_files/nvim/lazy-lock.json b/dot_files/nvim/lazy-lock.json deleted file mode 100644 index 26890c0..0000000 --- a/dot_files/nvim/lazy-lock.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "LazyVim": { "branch": "main", "commit": "28db03f958d58dfff3c647ce28fdc1cb88ac158d" }, - "SchemaStore.nvim": { "branch": "main", "commit": "e9c00ea7813006dfa29f35c174f83f0184d45a93" }, - "augment.vim": { "branch": "main", "commit": "97418c9dfc1918fa9bdd23863ea3d2e49130727f" }, - "blink.cmp": { "branch": "main", "commit": "b19413d214068f316c78978b08264ed1c41830ec" }, - "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, - "catppuccin": { "branch": "main", "commit": "ce4a8e0d5267e67056f9f4dcf6cb1d0933c8ca00" }, - "claudecode.nvim": { "branch": "main", "commit": "1552086ebcce9f4a2ea3b9793018a884d6b60169" }, - "conform.nvim": { "branch": "master", "commit": "4993e07fac6679d0a5005aa7499e0bad2bd39f19" }, - "crates.nvim": { "branch": "main", "commit": "ac9fa498a9edb96dc3056724ff69d5f40b898453" }, - "dial.nvim": { "branch": "master", "commit": "f97c0c7fa7d5111bc04a91d0f693900fb2d95861" }, - "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" }, - "friendly-snippets": { "branch": "main", "commit": "572f5660cf05f8cd8834e096d7b4c921ba18e175" }, - "gitsigns.nvim": { "branch": "main", "commit": "5813e4878748805f1518cee7abb50fd7205a3a48" }, - "grug-far.nvim": { "branch": "main", "commit": "b58b2d65863f4ebad88b10a1ddd519e5380466e0" }, - "helm-ls.nvim": { "branch": "main", "commit": "d6f3a8d4ad59b4f54cd734267dfb5411679ea608" }, - "inc-rename.nvim": { "branch": "main", "commit": "2597bccb57d1b570fbdbd4adf88b955f7ade715b" }, - "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, - "lazydev.nvim": { "branch": "main", "commit": "5231c62aa83c2f8dc8e7ba957aa77098cda1257d" }, - "lualine.nvim": { "branch": "master", "commit": "47f91c416daef12db467145e16bed5bbfe00add8" }, - "markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" }, - "mason-lspconfig.nvim": { "branch": "main", "commit": "0b9bb925c000ae649ff7e7149c8cd00031f4b539" }, - "mason.nvim": { "branch": "main", "commit": "57e5a8addb8c71fb063ee4acda466c7cf6ad2800" }, - "mini.ai": { "branch": "main", "commit": "bfb26d9072670c3aaefab0f53024b2f3729c8083" }, - "mini.hipatterns": { "branch": "main", "commit": "add8d8abad602787377ec5d81f6b248605828e0f" }, - "mini.icons": { "branch": "main", "commit": "ff2e4f1d29f659cc2bad0f9256f2f6195c6b2428" }, - "mini.pairs": { "branch": "main", "commit": "472ec50092a3314ec285d2db2baa48602d71fe93" }, - "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, - "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, - "nvim-lint": { "branch": "master", "commit": "d1118791070d090777398792a73032a0ca5c79ff" }, - "nvim-lspconfig": { "branch": "master", "commit": "effe4bf2e1afb881ea67291c648b68dd3dfc927a" }, - "nvim-treesitter": { "branch": "main", "commit": "e527584cf8508b2f99f127c2a20f93237e8fcf83" }, - "nvim-treesitter-textobjects": { "branch": "main", "commit": "dfbf9596f8aa8b4bed5301647485594ff7252955" }, - "nvim-ts-autotag": { "branch": "main", "commit": "c4ca798ab95b316a768d51eaaaee48f64a4a46bc" }, - "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, - "plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" }, - "render-markdown.nvim": { "branch": "main", "commit": "6e0e8902dac70fecbdd8ce557d142062a621ec38" }, - "rustaceanvim": { "branch": "master", "commit": "6c3785d6a230bec63f70c98bf8e2842bed924245" }, - "smear-cursor.nvim": { "branch": "main", "commit": "c85bdbb25db096fbcf616bc4e1357bd61fe2c199" }, - "snacks.nvim": { "branch": "main", "commit": "fe7cfe9800a182274d0f868a74b7263b8c0c020b" }, - "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, - "tokyonight.nvim": { "branch": "main", "commit": "5da1b76e64daf4c5d410f06bcb6b9cb640da7dfd" }, - "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, - "ts-comments.nvim": { "branch": "main", "commit": "123a9fb12e7229342f807ec9e6de478b1102b041" }, - "vim-dadbod": { "branch": "master", "commit": "e95afed23712f969f83b4857a24cf9d59114c2e6" }, - "vim-dadbod-completion": { "branch": "master", "commit": "a8dac0b3cf6132c80dc9b18bef36d4cf7a9e1fe6" }, - "vim-dadbod-ui": { "branch": "master", "commit": "48c4f271da13d380592f4907e2d1d5558044e4e5" }, - "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }, - "yanky.nvim": { "branch": "main", "commit": "04fc42b94305d94948c9c197f679336668af3292" } -} diff --git a/dot_files/quickshell/AppList.qml b/dot_files/quickshell/AppList.qml deleted file mode 100644 index 3c27def..0000000 --- a/dot_files/quickshell/AppList.qml +++ /dev/null @@ -1,118 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell - -Rectangle { - id: appListView - width: parent.width - height: visible ? Math.min(filteredApps.length * 52, 300) : 0 - color: "transparent" - - required property var filteredApps - required property bool isVisible - - visible: isVisible && filteredApps.length > 0 - - signal appLaunched(var app) - - property alias currentIndex: appList.currentIndex - - function moveUp() { - if (appList.currentIndex > 0) { - appList.currentIndex--; - } - } - - function moveDown() { - if (appList.currentIndex < filteredApps.length - 1) { - appList.currentIndex++; - } - } - - function launchCurrent() { - if (filteredApps.length > 0 && appList.currentIndex >= 0) { - appLaunched(filteredApps[appList.currentIndex]); - } - } - - ListView { - id: appList - anchors.fill: parent - clip: true - spacing: 4 - currentIndex: 0 - - model: appListView.filteredApps - - delegate: Rectangle { - required property var modelData - required property int index - - width: appList.width - height: 48 - radius: 6 - color: mouseArea.containsMouse || appList.currentIndex === index ? "#33467c" : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 12 - - // App icon - Image { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - source: modelData.icon ? "image://icon/" + modelData.icon : "" - sourceSize: Qt.size(32, 32) - - Text { - anchors.centerIn: parent - text: "" - font.family: "JetBrains Mono" - font.pixelSize: 20 - color: "#7aa2f7" - visible: parent.status !== Image.Ready - } - } - - // App name and description - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - Layout.fillWidth: true - text: modelData.name - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - elide: Text.ElideRight - } - - Text { - Layout.fillWidth: true - text: modelData.genericName || "" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - elide: Text.ElideRight - visible: text !== "" - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: appListView.appLaunched(modelData) - } - } - - ScrollBar.vertical: ScrollBar { - active: true - policy: ScrollBar.AsNeeded - } - } -} diff --git a/dot_files/quickshell/BluetoothView.qml b/dot_files/quickshell/BluetoothView.qml deleted file mode 100644 index 887a348..0000000 --- a/dot_files/quickshell/BluetoothView.qml +++ /dev/null @@ -1,464 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell.Io - -Rectangle { - id: bluetoothView - width: parent.width - height: visible ? Math.min(bluetoothContent.implicitHeight, 400) : 0 - color: "transparent" - clip: true - - required property bool isVisible - visible: isVisible - - property bool btPowered: false - property bool btScanning: false - property var btDevices: [] - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - // Refresh bluetooth status when view becomes visible - onVisibleChanged: { - if (visible) { - btStatusProcess.running = true; - btDevicesProcess.running = true; - } - } - - // Get bluetooth power status - Process { - id: btStatusProcess - command: ["bluetoothctl", "show"] - stdout: SplitParser { - onRead: (line) => { - if (line.includes("Powered:")) { - bluetoothView.btPowered = line.includes("yes"); - } - if (line.includes("Discovering:")) { - bluetoothView.btScanning = line.includes("yes"); - } - } - } - } - - // Get paired/known devices - Process { - id: btDevicesProcess - command: ["bluetoothctl", "devices"] - property string output: "" - stdout: SplitParser { - onRead: (line) => { - btDevicesProcess.output += line + "\n"; - } - } - onRunningChanged: { - if (!running && output !== "") { - var devices = []; - var lines = output.trim().split("\n"); - for (var i = 0; i < lines.length; i++) { - var match = lines[i].match(/Device ([0-9A-F:]+) (.+)/); - if (match) { - devices.push({ - address: match[1], - name: match[2], - connected: false, - paired: false - }); - } - } - // Now get info for each device - if (devices.length > 0) { - bluetoothView.btDevices = devices; - btInfoProcess.deviceIndex = 0; - btInfoProcess.runNextDevice(); - } else { - bluetoothView.btDevices = []; - } - output = ""; - } - } - } - - // Get detailed info for each device - Process { - id: btInfoProcess - property int deviceIndex: 0 - property string currentOutput: "" - - function runNextDevice() { - if (deviceIndex < bluetoothView.btDevices.length) { - currentOutput = ""; - command = ["bluetoothctl", "info", bluetoothView.btDevices[deviceIndex].address]; - running = true; - } - } - - stdout: SplitParser { - onRead: (line) => { - btInfoProcess.currentOutput += line + "\n"; - } - } - - onRunningChanged: { - if (!running && currentOutput !== "") { - var devices = bluetoothView.btDevices.slice(); - var dev = devices[deviceIndex]; - if (currentOutput.includes("Connected: yes")) { - dev.connected = true; - } - if (currentOutput.includes("Paired: yes")) { - dev.paired = true; - } - if (currentOutput.includes("Icon:")) { - var iconMatch = currentOutput.match(/Icon: (.+)/); - if (iconMatch) dev.icon = iconMatch[1].trim(); - } - devices[deviceIndex] = dev; - bluetoothView.btDevices = devices; - - deviceIndex++; - runNextDevice(); - } - } - } - - // Power on/off - Process { - id: btPowerProcess - property bool targetState: false - command: ["bluetoothctl", "power", targetState ? "on" : "off"] - onRunningChanged: { - if (!running) { - btStatusProcess.running = true; - } - } - } - - // Scan on/off - Process { - id: btScanProcess - property bool targetState: false - command: ["bluetoothctl", "scan", targetState ? "on" : "off"] - onRunningChanged: { - if (!running) { - btStatusProcess.running = true; - if (!targetState) { - btDevicesProcess.running = true; - } - } - } - } - - // Connect to device - Process { - id: btConnectProcess - property string targetAddress: "" - command: ["bluetoothctl", "connect", targetAddress] - onRunningChanged: { - if (!running) { - btDevicesProcess.running = true; - } - } - } - - // Disconnect from device - Process { - id: btDisconnectProcess - property string targetAddress: "" - command: ["bluetoothctl", "disconnect", targetAddress] - onRunningChanged: { - if (!running) { - btDevicesProcess.running = true; - } - } - } - - // Background refresh timer - updates device list every 10 seconds while visible - Timer { - interval: 10000 - running: bluetoothView.visible && bluetoothView.btPowered - repeat: true - onTriggered: btDevicesProcess.running = true - } - - Column { - id: bluetoothContent - width: parent.width - spacing: 8 - - // Adapter controls - Rectangle { - width: parent.width - height: 44 - radius: 6 - color: "#24283b" - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - - // Bluetooth icon - Text { - text: "󰂯" - font.family: "JetBrains Mono" - font.pixelSize: 18 - color: bluetoothView.btPowered ? "#7aa2f7" : "#565f89" - } - - // Status text - Text { - text: bluetoothView.btPowered ? "Bluetooth On" : "Bluetooth Off" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - - Item { Layout.fillWidth: true } - - // Scan button - Rectangle { - Layout.preferredWidth: scanText.implicitWidth + 16 - Layout.preferredHeight: 28 - radius: 4 - color: scanMouse.containsMouse ? "#33467c" : "transparent" - border.color: bluetoothView.btScanning ? "#7aa2f7" : "#33467c" - border.width: 1 - visible: bluetoothView.btPowered - - Text { - id: scanText - anchors.centerIn: parent - text: bluetoothView.btScanning ? "󰍰 Scanning..." : "󰍉 Scan" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: bluetoothView.btScanning ? "#7aa2f7" : "#c0caf5" - } - - MouseArea { - id: scanMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - btScanProcess.targetState = !bluetoothView.btScanning; - btScanProcess.running = true; - } - } - } - - // Power toggle - Rectangle { - Layout.preferredWidth: 48 - Layout.preferredHeight: 28 - radius: 14 - color: bluetoothView.btPowered ? "#7aa2f7" : "#33467c" - - Rectangle { - width: 22 - height: 22 - radius: 11 - color: "#c0caf5" - anchors.verticalCenter: parent.verticalCenter - x: bluetoothView.btPowered ? parent.width - width - 3 : 3 - - Behavior on x { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - btPowerProcess.targetState = !bluetoothView.btPowered; - btPowerProcess.running = true; - } - } - } - } - } - - // Devices list header - Text { - text: "Devices" - font.family: "JetBrains Mono" - font.pixelSize: 12 - font.bold: true - color: "#565f89" - visible: bluetoothView.btDevices.length > 0 - } - - // Device list - ListView { - id: bluetoothDeviceList - width: parent.width - height: Math.min(contentHeight, 280) - clip: true - spacing: 4 - visible: bluetoothView.btDevices.length > 0 - - model: bluetoothView.btDevices - - delegate: Rectangle { - required property var modelData - required property int index - - width: bluetoothDeviceList.width - height: 56 - radius: 6 - color: btDeviceMouse.containsMouse ? "#33467c" : "#24283b" - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - - // Device icon - Text { - text: { - var icon = modelData.icon || ""; - if (icon.includes("phone")) return "󰏲"; - if (icon.includes("audio") || icon.includes("headset") || icon.includes("headphone")) return "󰋋"; - if (icon.includes("keyboard")) return "󰌌"; - if (icon.includes("mouse")) return "󰍽"; - if (icon.includes("computer")) return "󰍹"; - return "󰂱"; - } - font.family: "JetBrains Mono" - font.pixelSize: 20 - color: modelData.connected ? "#7aa2f7" : "#565f89" - } - - // Device info - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - text: modelData.name || modelData.address - font.family: "JetBrains Mono" - font.pixelSize: 13 - color: "#c0caf5" - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: { - if (modelData.connected) return "Connected"; - if (modelData.paired) return "Paired"; - return "Available"; - } - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: modelData.connected ? "#7aa2f7" : "#565f89" - Layout.fillWidth: true - } - } - - // Connect/Disconnect button - Rectangle { - Layout.preferredWidth: connectBtnText.implicitWidth + 16 - Layout.preferredHeight: 28 - radius: 4 - color: connectBtnMouse.containsMouse ? (modelData.connected ? "#f7768e" : "#7aa2f7") : "transparent" - border.color: modelData.connected ? "#f7768e" : "#7aa2f7" - border.width: 1 - - Text { - id: connectBtnText - anchors.centerIn: parent - text: modelData.connected ? "Disconnect" : "Connect" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: connectBtnMouse.containsMouse ? "#1a1b26" : (modelData.connected ? "#f7768e" : "#7aa2f7") - } - - MouseArea { - id: connectBtnMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.connected) { - btDisconnectProcess.targetAddress = modelData.address; - btDisconnectProcess.running = true; - } else { - btConnectProcess.targetAddress = modelData.address; - btConnectProcess.running = true; - } - } - } - } - } - - MouseArea { - id: btDeviceMouse - anchors.fill: parent - hoverEnabled: true - z: -1 - } - } - - ScrollBar.vertical: ScrollBar { - active: true - policy: ScrollBar.AsNeeded - } - } - - // Empty state - Rectangle { - width: parent.width - height: 80 - radius: 6 - color: "#24283b" - visible: bluetoothView.btDevices.length === 0 && bluetoothView.btPowered - - Column { - anchors.centerIn: parent - spacing: 8 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: bluetoothView.btScanning ? "Scanning for devices..." : "No devices found" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#565f89" - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "Click Scan to discover nearby devices" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - visible: !bluetoothView.btScanning - } - } - } - - // Adapter disabled state - Rectangle { - width: parent.width - height: 60 - radius: 6 - color: "#24283b" - visible: !bluetoothView.btPowered - - Text { - anchors.centerIn: parent - text: "Turn on Bluetooth to see devices" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#565f89" - } - } - } -} diff --git a/dot_files/quickshell/CalendarView.qml b/dot_files/quickshell/CalendarView.qml deleted file mode 100644 index 3137932..0000000 --- a/dot_files/quickshell/CalendarView.qml +++ /dev/null @@ -1,189 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -Rectangle { - id: calendarView - width: parent.width - height: visible ? calendarContent.implicitHeight : 0 - color: "transparent" - clip: true - - required property bool isVisible - visible: isVisible - - property int calendarMonth: new Date().getMonth() - property int calendarYear: new Date().getFullYear() - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - function reset() { - var now = new Date(); - calendarMonth = now.getMonth(); - calendarYear = now.getFullYear(); - } - - Column { - id: calendarContent - width: parent.width - spacing: 8 - - // Month/Year header with navigation - Rectangle { - width: parent.width - height: 36 - radius: 6 - color: "#24283b" - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - - // Previous month - Rectangle { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - radius: 4 - color: prevMonthMouse.containsMouse ? "#33467c" : "transparent" - - Text { - anchors.centerIn: parent - text: "<" - font.family: "JetBrains Mono" - font.pixelSize: 16 - font.bold: true - color: "#c0caf5" - } - - MouseArea { - id: prevMonthMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (calendarView.calendarMonth === 0) { - calendarView.calendarMonth = 11; - calendarView.calendarYear--; - } else { - calendarView.calendarMonth--; - } - } - } - } - - Item { Layout.fillWidth: true } - - // Month Year display - Text { - text: new Date(calendarView.calendarYear, calendarView.calendarMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") - font.family: "JetBrains Mono" - font.pixelSize: 14 - font.bold: true - color: "#c0caf5" - } - - Item { Layout.fillWidth: true } - - // Next month - Rectangle { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - radius: 4 - color: nextMonthMouse.containsMouse ? "#33467c" : "transparent" - - Text { - anchors.centerIn: parent - text: ">" - font.family: "JetBrains Mono" - font.pixelSize: 16 - font.bold: true - color: "#c0caf5" - } - - MouseArea { - id: nextMonthMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (calendarView.calendarMonth === 11) { - calendarView.calendarMonth = 0; - calendarView.calendarYear++; - } else { - calendarView.calendarMonth++; - } - } - } - } - } - } - - // Day headers - Row { - width: parent.width - - Repeater { - model: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] - delegate: Item { - width: parent.width / 7 - height: 24 - - Text { - anchors.centerIn: parent - text: modelData - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - } - } - } - } - - // Calendar grid - Grid { - id: calendarGrid - width: parent.width - columns: 7 - - property var firstDay: new Date(calendarView.calendarYear, calendarView.calendarMonth, 1) - property int startDay: firstDay.getDay() - property int daysInMonth: new Date(calendarView.calendarYear, calendarView.calendarMonth + 1, 0).getDate() - property var today: new Date() - - Repeater { - model: 42 - - delegate: Item { - width: calendarGrid.width / 7 - height: 32 - - property int dayNum: index - calendarGrid.startDay + 1 - property bool isCurrentMonth: dayNum > 0 && dayNum <= calendarGrid.daysInMonth - property bool isToday: isCurrentMonth && - dayNum === calendarGrid.today.getDate() && - calendarView.calendarMonth === calendarGrid.today.getMonth() && - calendarView.calendarYear === calendarGrid.today.getFullYear() - - Rectangle { - anchors.centerIn: parent - width: 28 - height: 28 - radius: 14 - color: isToday ? "#7aa2f7" : "transparent" - visible: isCurrentMonth - - Text { - anchors.centerIn: parent - text: isCurrentMonth ? dayNum : "" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: isToday ? "#1a1b26" : "#c0caf5" - } - } - } - } - } - } -} diff --git a/dot_files/quickshell/GlobalStates.qml b/dot_files/quickshell/GlobalStates.qml new file mode 100644 index 0000000..cb25668 --- /dev/null +++ b/dot_files/quickshell/GlobalStates.qml @@ -0,0 +1,212 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Centralized state management singleton +// All global UI states are managed here to prevent scattered state definitions +Singleton { + id: root + + // Active screen for overlays (sidebars, launcher, etc.) + property var activeScreen: null + + // Screen position tracking for multi-monitor layouts + // The leftmost screen shows the app launcher, rightmost shows notifications/clock + property var leftmostScreen: null + property var rightmostScreen: null + + // Determine leftmost and rightmost screens using Hyprland monitor data + function updateScreenPositions() { + let screens = Quickshell.screens + if (!screens || screens.length === 0) return + + // For single monitor, it's both left and right + if (screens.length === 1) { + leftmostScreen = screens[0] + rightmostScreen = screens[0] + return + } + + // Query Hyprland for monitor positions + monitorQuery.running = true + } + + // Process to get Hyprland monitor info + Process { + id: monitorQuery + command: ["hyprctl", "monitors", "-j"] + + stdout: SplitParser { + splitMarker: "" + onRead: data => { + if (!data || data.trim() === "") return + + try { + const monitors = JSON.parse(data) + let minX = 999999 + let maxX = -999999 + let leftName = "" + let rightName = "" + + for (const mon of monitors) { + if (mon.x < minX) { + minX = mon.x + leftName = mon.name + } + if (mon.x > maxX) { + maxX = mon.x + rightName = mon.name + } + } + + // Find matching Quickshell screens by name + for (let i = 0; i < Quickshell.screens.length; i++) { + const s = Quickshell.screens[i] + if (s.name === leftName) { + root.leftmostScreen = s + } + if (s.name === rightName) { + root.rightmostScreen = s + } + } + } catch (e) { + console.log("Failed to parse monitor data:", e) + } + } + } + } + + // Check if a screen is the leftmost + function isLeftmostScreen(screen): bool { + if (!screen) return false + // Single monitor case + if (Quickshell.screens.length === 1) return true + return screen === leftmostScreen + } + + // Check if a screen is the rightmost + function isRightmostScreen(screen): bool { + if (!screen) return false + // Single monitor case + if (Quickshell.screens.length === 1) return true + return screen === rightmostScreen + } + + // Update screen positions on startup and when screens change + Component.onCompleted: { + // Delay to ensure screens are available + Qt.callLater(updateScreenPositions) + } + + // Watch for screen changes + Connections { + target: Quickshell + function onScreensChanged() { + root.updateScreenPositions() + } + } + + // Also update when screens list changes length + property int screenCount: Quickshell.screens.length + onScreenCountChanged: { + updateScreenPositions() + } + + // Sidebar visibility + property bool sidebarLeftOpen: false + property bool sidebarRightOpen: false + + // Right sidebar view mode: "default", "bluetooth", "audio", "calendar", "notifications" + property string sidebarRightView: "default" + + // OSD (On-Screen Display) state + property bool osdVisible: false + property string osdType: "volume" // "volume", "brightness", "mic" + property real osdValue: 0.0 + property bool osdMuted: false + + // Notification states + property int unreadNotificationCount: 0 + property bool doNotDisturb: false + + // Bar states + property bool barExpanded: false + + // Settings panel + property bool settingsOpen: false + + // Welcome wizard (first-run) + property bool welcomeActive: false + + // Screen zoom (accessibility) + property real screenZoom: 1.0 + Behavior on screenZoom { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + // Super key tracking (for workspace numbers, etc.) + property bool superDown: false + property bool superReleaseMightTrigger: false + + // Close other panels when left sidebar opens + onSidebarLeftOpenChanged: { + if (sidebarLeftOpen) { + sidebarRightOpen = false + } + } + + // Mutual exclusion for right sidebar + onSidebarRightOpenChanged: { + if (sidebarRightOpen) { + sidebarLeftOpen = false + } + } + + // OSD auto-hide timer + Timer { + id: osdHideTimer + interval: 1500 + repeat: false + onTriggered: osdVisible = false + } + + onOsdVisibleChanged: { + if (osdVisible) { + osdHideTimer.restart() + } + } + + // Function to toggle panels (accepts screen to show on) + function toggleSidebarLeft(screen) { + if (sidebarLeftOpen && activeScreen === screen) { + sidebarLeftOpen = false + } else { + activeScreen = screen + sidebarLeftOpen = true + } + } + + function toggleSidebarRight(screen, view) { + const targetView = view || "default" + if (sidebarRightOpen && activeScreen === screen && sidebarRightView === targetView) { + sidebarRightOpen = false + sidebarRightView = "default" + } else { + activeScreen = screen + sidebarRightView = targetView + sidebarRightOpen = true + } + } + + function closeAll() { + sidebarLeftOpen = false + sidebarRightOpen = false + sidebarRightView = "default" + settingsOpen = false + } +} diff --git a/dot_files/quickshell/HelpView.qml b/dot_files/quickshell/HelpView.qml deleted file mode 100644 index 450a628..0000000 --- a/dot_files/quickshell/HelpView.qml +++ /dev/null @@ -1,166 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -Rectangle { - id: helpView - width: parent.width - height: visible ? helpContent.implicitHeight : 0 - color: "transparent" - clip: true - - required property bool isVisible - visible: isVisible - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - Column { - id: helpContent - width: parent.width - spacing: 8 - - // Commands section - Rectangle { - width: parent.width - height: commandsCol.implicitHeight + 16 - radius: 6 - color: "#24283b" - - Column { - id: commandsCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 6 - - Text { - text: "Commands" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#7aa2f7" - } - - // Command list - Repeater { - model: [ - { cmd: "/? or /help", desc: "Show this help" }, - { cmd: "/c or /calendar", desc: "Show calendar" }, - { cmd: "/n or /notifications", desc: "Show notifications" }, - { cmd: "/b or /bluetooth", desc: "Bluetooth controls" } - ] - - Row { - spacing: 12 - Text { - width: 140 - text: modelData.cmd - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#9ece6a" - } - Text { - text: modelData.desc - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#c0caf5" - } - } - } - } - } - - // Keyboard shortcuts section - Rectangle { - width: parent.width - height: shortcutsCol.implicitHeight + 16 - radius: 6 - color: "#24283b" - - Column { - id: shortcutsCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 6 - - Text { - text: "Keyboard Shortcuts" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#7aa2f7" - } - - Repeater { - model: [ - { key: "Ctrl+C", desc: "Open calendar" }, - { key: "Ctrl+N", desc: "Open notifications" }, - { key: "Ctrl+B", desc: "Open bluetooth" }, - { key: "Ctrl+J", desc: "Focus list (in notifications)" }, - { key: "Ctrl+K", desc: "Return to input" }, - { key: "Escape", desc: "Clear input / close HUD" }, - { key: "↑ / ↓", desc: "Navigate list" }, - { key: "j / k", desc: "Navigate list (vim)" }, - { key: "Enter", desc: "Launch app / confirm" }, - { key: "d / x", desc: "Dismiss notification" } - ] - - Row { - spacing: 12 - Text { - width: 140 - text: modelData.key - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#ff9e64" - } - Text { - text: modelData.desc - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#c0caf5" - } - } - } - } - } - - // Tips section - Rectangle { - width: parent.width - height: tipsCol.implicitHeight + 16 - radius: 6 - color: "#24283b" - - Column { - id: tipsCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 6 - - Text { - text: "Tips" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#7aa2f7" - } - - Text { - width: parent.width - text: "• Type to search applications\n• Click date/time to toggle calendar\n• Click bell icon to toggle notifications" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#c0caf5" - lineHeight: 1.4 - } - } - } - } -} diff --git a/dot_files/quickshell/Hud.qml b/dot_files/quickshell/Hud.qml deleted file mode 100644 index dc427eb..0000000 --- a/dot_files/quickshell/Hud.qml +++ /dev/null @@ -1,431 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Io -import Quickshell.Wayland - -import "hud" - -Scope { - id: hudScope - - property bool showing: false - property var notificationList: [] - signal close() - signal dismissNotification(var notification) - signal clearAllNotifications() - - // Weather settings - empty string = auto-detect by IP - property string weatherLocation: "" - - PanelWindow { - id: hud - - visible: hudScope.showing - - anchors { - top: true - bottom: true - left: true - right: true - } - - exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - - color: "transparent" - - // Search state - property string searchText: "" - property var filteredApps: [] - - // Active view: "default", "calendar", "notifications", "bluetooth", "help" - property string activeView: "default" - - // Focus state: "input" or "notifications" - property string focusState: "input" - - // DateTime - property string currentTime: "" - property string currentDate: "" - - // Weather - property string weatherTemp: "" - property string weatherIcon: "" - property string weatherDesc: "" - - // View control functions - function showCalendar() { - activeView = "calendar"; - calendarView.reset(); - } - - function hideCalendar() { - activeView = "default"; - } - - function showNotifications() { - activeView = "notifications"; - } - - function hideNotifications() { - activeView = "default"; - } - - function showBluetooth() { - activeView = "bluetooth"; - } - - function hideBluetooth() { - activeView = "default"; - } - - function showHelp() { - activeView = "help"; - } - - function hideHelp() { - activeView = "default"; - } - - function handleCommand(cmd) { - var c = cmd.toLowerCase().trim(); - if (c === "/c" || c === "/calendar") { - showCalendar(); - return true; - } - if (c === "/n" || c === "/notifications") { - showNotifications(); - return true; - } - if (c === "/b" || c === "/bluetooth") { - showBluetooth(); - return true; - } - if (c === "/?" || c === "/help") { - showHelp(); - return true; - } - return false; - } - - // DateTime update timer - Timer { - interval: 1000 - running: true - repeat: true - triggeredOnStart: true - onTriggered: { - var now = new Date(); - hud.currentTime = now.toLocaleTimeString(Qt.locale(), "h:mm ap"); - hud.currentDate = now.toLocaleDateString(Qt.locale(), "dddd, MMMM d"); - } - } - - // Weather fetch - runs every 15 minutes - Timer { - interval: 900000 - running: true - repeat: true - triggeredOnStart: true - onTriggered: weatherProcess.running = true - } - - Process { - id: weatherProcess - command: ["curl", "-sf", "wttr.in/" + hudScope.weatherLocation + "?format=%t|%C"] - stdout: SplitParser { - onRead: (line) => { - var parts = line.split("|"); - if (parts.length >= 2) { - hud.weatherTemp = parts[0].trim(); - hud.weatherDesc = parts[1].trim(); - // Map conditions to icons - var desc = hud.weatherDesc.toLowerCase(); - if (desc.includes("sun") || desc.includes("clear")) { - hud.weatherIcon = ""; - } else if (desc.includes("cloud") || desc.includes("overcast")) { - hud.weatherIcon = ""; - } else if (desc.includes("rain") || desc.includes("drizzle")) { - hud.weatherIcon = ""; - } else if (desc.includes("snow")) { - hud.weatherIcon = ""; - } else if (desc.includes("thunder") || desc.includes("storm")) { - hud.weatherIcon = ""; - } else if (desc.includes("fog") || desc.includes("mist")) { - hud.weatherIcon = ""; - } else if (desc.includes("partly")) { - hud.weatherIcon = ""; - } else { - hud.weatherIcon = ""; - } - } - } - } - } - - function updateFilteredApps() { - var apps = []; - var search = searchText.toLowerCase(); - - for (var i = 0; i < DesktopEntries.applications.values.length; i++) { - var app = DesktopEntries.applications.values[i]; - if (!app.noDisplay && ( - app.name.toLowerCase().includes(search) || - (app.genericName && app.genericName.toLowerCase().includes(search)) || - (app.comment && app.comment.toLowerCase().includes(search)))) { - apps.push(app); - } - } - - // Sort alphabetically - apps.sort(function(a, b) { - return a.name.localeCompare(b.name); - }); - - filteredApps = apps.slice(0, 50); // Limit results - } - - function hide() { - searchText = ""; - searchInput.text = ""; - activeView = "default"; - focusState = "input"; - hudScope.close(); - } - - function launchApp(app) { - app.execute(); - hide(); - } - - onVisibleChanged: { - if (visible) { - searchText = ""; - searchInput.text = ""; - filteredApps = []; - activeView = "default"; - focusState = "input"; - focusTimer.start(); - } - } - - Timer { - id: focusTimer - interval: 50 - onTriggered: searchInput.forceActiveFocus() - } - - // Click outside to close - MouseArea { - anchors.fill: parent - onClicked: (mouse) => { - // Calculate hudContainer bounds - var containerLeft = hudContainer.x; - var containerRight = hudContainer.x + hudContainer.width; - var containerTop = hudContainer.y; - var containerBottom = hudContainer.y + hudContainer.height; - - // Only close if click is outside the container - if (mouse.x < containerLeft || mouse.x > containerRight || - mouse.y < containerTop || mouse.y > containerBottom) { - hud.hide(); - } - } - } - - // Main HUD container - Rectangle { - id: hudContainer - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: parent.height * 0.15 - width: 700 - height: contentColumn.implicitHeight + 16 - radius: 12 - color: "#1a1b26" - border.color: "#33467c" - border.width: 1 - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - Column { - id: contentColumn - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 8 - - // Status bar - StatusBar { - activeView: hud.activeView - notificationCount: hudScope.notificationList.length - currentDate: hud.currentDate - currentTime: hud.currentTime - weatherTemp: hud.weatherTemp - weatherIcon: hud.weatherIcon - - onToggleNotifications: { - if (hud.activeView === "notifications") { - hud.hideNotifications(); - } else { - hud.showNotifications(); - } - } - - onToggleCalendar: { - if (hud.activeView === "calendar") { - hud.hideCalendar(); - } else { - hud.showCalendar(); - } - } - } - - // Command input - Rectangle { - id: searchBox - width: parent.width - height: 48 - radius: 8 - color: "#24283b" - border.color: searchInput.activeFocus ? "#7aa2f7" : "#33467c" - border.width: 1 - - RowLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 8 - - Text { - text: ">" - font.family: "JetBrains Mono" - font.pixelSize: 18 - color: "#7aa2f7" - } - - TextInput { - id: searchInput - Layout.fillWidth: true - font.family: "JetBrains Mono" - font.pixelSize: 16 - color: "#c0caf5" - selectionColor: "#33467c" - selectedTextColor: "#c0caf5" - - onTextChanged: { - hud.searchText = text; - if (text && !text.startsWith("/")) { - // Switch to default view to show app list - hud.activeView = "default"; - hud.updateFilteredApps(); - } else { - hud.filteredApps = []; - } - appListView.currentIndex = 0; - } - - Keys.onEscapePressed: { - if (text !== "") { - text = ""; - } else { - hud.hide(); - } - } - Keys.onReturnPressed: { - // Check for commands first - if (hud.handleCommand(text)) { - return; - } - if (hud.filteredApps.length > 0) { - hud.launchApp(hud.filteredApps[appListView.currentIndex >= 0 ? appListView.currentIndex : 0]); - } - } - Keys.onPressed: (event) => { - if (event.modifiers & Qt.ControlModifier) { - // Ctrl+C for calendar - if (event.key === Qt.Key_C) { - hud.showCalendar(); - event.accepted = true; - } - // Ctrl+N for notifications - else if (event.key === Qt.Key_N) { - hud.showNotifications(); - event.accepted = true; - } - // Ctrl+B for bluetooth - else if (event.key === Qt.Key_B) { - hud.showBluetooth(); - event.accepted = true; - } - // Ctrl+J to move focus to notification list - else if (event.key === Qt.Key_J && hud.activeView === "notifications" && hudScope.notificationList.length > 0) { - hud.focusState = "notifications"; - notificationsView.focusList(); - event.accepted = true; - } - } - } - Keys.onDownPressed: appListView.moveDown() - Keys.onUpPressed: appListView.moveUp() - - Text { - anchors.fill: parent - text: "Enter command..." - font: parent.font - color: "#565f89" - visible: !parent.text - } - } - } - } - - // App list view - AppList { - id: appListView - filteredApps: hud.filteredApps - isVisible: hud.activeView === "default" && hud.searchText && !hud.searchText.startsWith("/") - - onAppLaunched: (app) => hud.launchApp(app) - } - - // Calendar view - CalendarView { - id: calendarView - isVisible: hud.activeView === "calendar" - } - - // Notifications view - NotificationsView { - id: notificationsView - isVisible: hud.activeView === "notifications" - notificationList: hudScope.notificationList - - onDismissNotification: (notification) => hudScope.dismissNotification(notification) - onClearAllNotifications: hudScope.clearAllNotifications() - onRequestFocusInput: { - hud.focusState = "input"; - searchInput.forceActiveFocus(); - } - } - - // Bluetooth view - BluetoothView { - id: bluetoothView - isVisible: hud.activeView === "bluetooth" - } - - // Help view - HelpView { - id: helpView - isVisible: hud.activeView === "help" - } - } - } - } -} diff --git a/dot_files/quickshell/NotificationsView.qml b/dot_files/quickshell/NotificationsView.qml deleted file mode 100644 index dfd5602..0000000 --- a/dot_files/quickshell/NotificationsView.qml +++ /dev/null @@ -1,331 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls - -Rectangle { - id: notificationsView - width: parent.width - height: visible ? Math.min(notificationsContent.implicitHeight, 350) : 0 - color: "transparent" - clip: true - - required property bool isVisible - required property var notificationList - - visible: isVisible - - signal dismissNotification(var notification) - signal clearAllNotifications() - signal requestFocusInput() - - property alias currentIndex: notificationListView.currentIndex - property bool hasSelection: false - property bool clearAllSelected: false - property int lastSelectedIndex: 0 - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - function focusList() { - if (notificationList.length > 0) { - hasSelection = true; - clearAllSelected = false; - // Restore last selected, clamped to valid range - var idx = Math.min(lastSelectedIndex, notificationList.length - 1); - notificationListView.currentIndex = idx; - notificationListView.positionViewAtIndex(idx, ListView.Contain); - notificationListView.forceActiveFocus(); - } - } - - function selectAndScroll(newIndex) { - if (newIndex >= 0 && newIndex < notificationListView.count) { - hasSelection = true; - clearAllSelected = false; - lastSelectedIndex = newIndex; - notificationListView.currentIndex = newIndex; - notificationListView.positionViewAtIndex(newIndex, ListView.Contain); - } - } - - function selectClearAll() { - hasSelection = false; - clearAllSelected = true; - } - - function moveUp() { - if (clearAllSelected) { - // Move from Clear All back to last notification - clearAllSelected = false; - hasSelection = true; - notificationListView.currentIndex = notificationListView.count - 1; - notificationListView.positionViewAtIndex(notificationListView.currentIndex, ListView.Contain); - } else if (!hasSelection) { - selectAndScroll(0); - } else if (currentIndex > 0) { - selectAndScroll(currentIndex - 1); - } - } - - function moveDown() { - if (clearAllSelected) { - // Already at bottom, do nothing - return; - } else if (!hasSelection) { - selectAndScroll(0); - } else if (currentIndex < notificationListView.count - 1) { - selectAndScroll(currentIndex + 1); - } else if (currentIndex === notificationListView.count - 1) { - // At last notification, move to Clear All - selectClearAll(); - } - } - - function exitUp() { - // Ctrl+K - exit up - if (clearAllSelected) { - // From Clear All, go to bottom of list - clearAllSelected = false; - hasSelection = true; - notificationListView.currentIndex = notificationListView.count - 1; - notificationListView.positionViewAtIndex(notificationListView.currentIndex, ListView.Contain); - } else { - // From list, go to text box - hasSelection = false; - clearAllSelected = false; - requestFocusInput(); - } - } - - function exitDown() { - // Ctrl+J - exit to Clear All - hasSelection = false; - selectClearAll(); - } - - function dismissCurrentAndSelectNext() { - if (!hasSelection || currentIndex < 0 || currentIndex >= notificationList.length) { - return; - } - var dismissIdx = currentIndex; - var nextIdx = dismissIdx; - // If we're at the last item, select the previous one - if (dismissIdx >= notificationList.length - 1) { - nextIdx = Math.max(0, dismissIdx - 1); - } - lastSelectedIndex = nextIdx; - var notif = notificationList[dismissIdx]; - dismissNotification(notif); - // After dismiss, list will be one shorter - update current index - // Use Qt.callLater to ensure list has updated first - Qt.callLater(function() { - if (notificationList.length > 0) { - var clampedIdx = Math.min(nextIdx, notificationList.length - 1); - notificationListView.currentIndex = clampedIdx; - lastSelectedIndex = clampedIdx; - notificationListView.positionViewAtIndex(clampedIdx, ListView.Contain); - } else { - hasSelection = false; - } - }); - } - - Column { - id: notificationsContent - width: parent.width - spacing: 4 - - // Empty state - Rectangle { - width: parent.width - height: 60 - radius: 6 - color: "#24283b" - visible: notificationsView.notificationList.length === 0 - - Text { - anchors.centerIn: parent - text: "No notifications" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#565f89" - } - } - - // Notification list - ListView { - id: notificationListView - width: parent.width - height: Math.min(contentHeight, 300) - clip: true - spacing: 4 - visible: notificationsView.notificationList.length > 0 - highlightFollowsCurrentItem: false - highlight: null - - model: notificationsView.notificationList - - delegate: Rectangle { - required property var modelData - required property int index - - width: notificationListView.width - height: notifContent.implicitHeight + 16 - radius: 6 - color: notifMouse.containsMouse || (notificationsView.hasSelection && notificationListView.currentIndex === index) ? "#33467c" : "#24283b" - - Column { - id: notifContent - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 4 - - RowLayout { - width: parent.width - spacing: 8 - - Text { - text: modelData.summary || modelData.appName || "Notification" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#c0caf5" - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: "" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: dismissNotifMouse.containsMouse ? "#f7768e" : "#565f89" - - MouseArea { - id: dismissNotifMouse - anchors.fill: parent - anchors.margins: -4 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: notificationsView.dismissNotification(modelData) - } - } - } - - Text { - width: parent.width - text: modelData.body || "" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - visible: text !== "" - } - } - - MouseArea { - id: notifMouse - anchors.fill: parent - hoverEnabled: true - z: -1 - onClicked: { - notificationsView.hasSelection = true; - notificationListView.currentIndex = index; - } - } - } - - // Keyboard navigation - Keys.onUpPressed: notificationsView.moveUp() - Keys.onDownPressed: notificationsView.moveDown() - Keys.onReturnPressed: { - if (notificationsView.clearAllSelected) { - notificationsView.clearAllNotifications(); - } else if (notificationsView.hasSelection && currentIndex >= 0 && currentIndex < notificationsView.notificationList.length) { - // Invoke default action (first action) on Enter - var notif = notificationsView.notificationList[currentIndex]; - if (notif.actions && notif.actions.length > 0) { - notif.actions[0].invoke(); - // If not resident, notification auto-dismisses, select next - if (!notif.resident) { - notificationsView.dismissCurrentAndSelectNext(); - } - } - } - } - Keys.onPressed: (event) => { - if (event.modifiers & Qt.ControlModifier) { - // Ctrl+K - exit up to text box - if (event.key === Qt.Key_K) { - notificationsView.exitUp(); - event.accepted = true; - } - // Ctrl+J - exit down to Clear All - else if (event.key === Qt.Key_J) { - notificationsView.exitDown(); - event.accepted = true; - } - } else { - // Vim bindings (no modifier) - if (event.key === Qt.Key_K) { - notificationsView.moveUp(); - event.accepted = true; - } else if (event.key === Qt.Key_J) { - notificationsView.moveDown(); - event.accepted = true; - } else if (event.key === Qt.Key_G && (event.modifiers & Qt.ShiftModifier)) { - // Shift+G - go to last notification - notificationsView.selectAndScroll(notificationListView.count - 1); - event.accepted = true; - } else if (event.key === Qt.Key_G) { - // g - go to top - notificationsView.selectAndScroll(0); - event.accepted = true; - } else if (event.key === Qt.Key_D || event.key === Qt.Key_X || - event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { - // Dismiss current and select next - notificationsView.dismissCurrentAndSelectNext(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - notificationsView.exitUp(); - event.accepted = true; - } - } - } - - ScrollBar.vertical: ScrollBar { - active: true - policy: ScrollBar.AsNeeded - } - } - - // Clear all button (at bottom) - Rectangle { - width: parent.width - height: 28 - radius: 6 - color: clearAllMouse.containsMouse || notificationsView.clearAllSelected ? "#f7768e" : "#24283b" - visible: notificationsView.notificationList.length > 0 - - Text { - anchors.centerIn: parent - text: " Clear All (" + notificationsView.notificationList.length + ")" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: clearAllMouse.containsMouse || notificationsView.clearAllSelected ? "#1a1b26" : "#565f89" - } - - MouseArea { - id: clearAllMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: notificationsView.clearAllNotifications() - } - } - } -} diff --git a/dot_files/quickshell/Osd.qml b/dot_files/quickshell/Osd.qml deleted file mode 100644 index 039de1e..0000000 --- a/dot_files/quickshell/Osd.qml +++ /dev/null @@ -1,240 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Quickshell.Services.Pipewire -import Quickshell.Widgets - -Scope { - id: osdRoot - - property bool shouldShow: false - property string osdType: "volume" // "volume", "brightness", "mic" - property real osdValue: 0 - property bool osdMuted: false - property string osdIcon: "" - property bool initialized: false - - // Delay initialization to ignore startup signals - Timer { - id: initTimer - interval: 500 - running: true - onTriggered: osdRoot.initialized = true - } - - // Track Pipewire audio sink - PwObjectTracker { - objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource] - } - - // Track sink volume/mute changes - property real sinkVolume: Pipewire.defaultAudioSink?.audio?.averageVolume ?? 0 - property bool sinkMuted: Pipewire.defaultAudioSink?.audio?.muted ?? false - - onSinkVolumeChanged: { - if (initialized) - showOsd("volume", sinkVolume, sinkMuted); - } - onSinkMutedChanged: { - if (initialized) - showOsd("volume", sinkVolume, sinkMuted); - } - - // Track source volume/mute changes - property real sourceVolume: Pipewire.defaultAudioSource?.audio?.averageVolume ?? 0 - property bool sourceMuted: Pipewire.defaultAudioSource?.audio?.muted ?? false - - onSourceVolumeChanged: { - if (initialized) - showOsd("mic", sourceVolume, sourceMuted); - } - onSourceMutedChanged: { - if (initialized) - showOsd("mic", sourceVolume, sourceMuted); - } - - // IPC handler for OSD commands - IpcHandler { - target: "osd" - - function volumeUp() { - if (Pipewire.defaultAudioSink?.audio) { - var newVol = Math.min(1.0, Pipewire.defaultAudioSink.audio.averageVolume + 0.05); - Pipewire.defaultAudioSink.audio.averageVolume = newVol; - } - } - - function volumeDown() { - if (Pipewire.defaultAudioSink?.audio) { - var newVol = Math.max(0.0, Pipewire.defaultAudioSink.audio.averageVolume - 0.05); - Pipewire.defaultAudioSink.audio.averageVolume = newVol; - } - } - - function volumeMute() { - if (Pipewire.defaultAudioSink?.audio) { - Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted; - } - } - - function micMute() { - if (Pipewire.defaultAudioSource?.audio) { - Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted; - } - } - - function brightnessUp() { - brightnessProcess.command = ["brightnessctl", "set", "+5%"]; - brightnessProcess.running = true; - } - - function brightnessDown() { - brightnessProcess.command = ["brightnessctl", "set", "5%-"]; - brightnessProcess.running = true; - } - } - - // Brightness control process - Process { - id: brightnessProcess - onExited: { - // Read current brightness after change - getBrightnessProcess.running = true; - } - } - - Process { - id: getBrightnessProcess - command: ["brightnessctl", "-m"] - stdout: SplitParser { - onRead: (line) => { - // Format: device,class,current,max,percentage% - var parts = line.split(","); - if (parts.length >= 5) { - var percent = parseInt(parts[4].replace("%", "")) / 100; - osdRoot.showOsd("brightness", percent, false); - } - } - } - } - - function showOsd(type, value, muted) { - osdType = type; - osdValue = value; - osdMuted = muted; - - // Set icon based on type and state - if (type === "volume") { - if (muted) { - osdIcon = "󰝟"; - } else if (value > 0.66) { - osdIcon = "󰕾"; - } else if (value > 0.33) { - osdIcon = "󰖀"; - } else if (value > 0) { - osdIcon = "󰕿"; - } else { - osdIcon = "󰝟"; - } - } else if (type === "brightness") { - if (value > 0.66) { - osdIcon = "󰃠"; - } else if (value > 0.33) { - osdIcon = "󰃟"; - } else { - osdIcon = "󰃞"; - } - } else if (type === "mic") { - osdIcon = muted ? "󰍭" : "󰍬"; - } - - shouldShow = true; - hideTimer.restart(); - } - - Timer { - id: hideTimer - interval: 1500 - onTriggered: osdRoot.shouldShow = false - } - - // OSD Window - LazyLoader { - active: osdRoot.shouldShow - - PanelWindow { - anchors.bottom: true - margins.bottom: screen.height / 6 - - exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Overlay - - implicitWidth: 300 - implicitHeight: 60 - color: "transparent" - - // Don't block input - mask: Region {} - - Rectangle { - anchors.fill: parent - radius: 12 - color: "#e01a1b26" - border.color: "#33467c" - border.width: 1 - - RowLayout { - anchors { - fill: parent - leftMargin: 16 - rightMargin: 16 - } - spacing: 16 - - // Icon - Text { - text: osdRoot.osdIcon - font.family: "JetBrainsMono Nerd Font" - font.pixelSize: 28 - color: osdRoot.osdMuted ? "#f7768e" : "#7aa2f7" - } - - // Progress bar - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 8 - radius: 4 - color: "#33467c" - - Rectangle { - anchors { - left: parent.left - top: parent.top - bottom: parent.bottom - } - width: parent.width * osdRoot.osdValue - radius: parent.radius - color: osdRoot.osdMuted ? "#f7768e" : "#7aa2f7" - - Behavior on width { - NumberAnimation { duration: 100 } - } - } - } - - // Percentage - Text { - text: Math.round(osdRoot.osdValue * 100) + "%" - font.family: "JetBrainsMono Nerd Font" - font.pixelSize: 14 - color: "#c0caf5" - Layout.preferredWidth: 45 - horizontalAlignment: Text.AlignRight - } - } - } - } - } -} diff --git a/dot_files/quickshell/StatusBar.qml b/dot_files/quickshell/StatusBar.qml deleted file mode 100644 index b0e6a5c..0000000 --- a/dot_files/quickshell/StatusBar.qml +++ /dev/null @@ -1,214 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Services.SystemTray - -Rectangle { - id: statusBar - width: parent.width - height: 40 - radius: 8 - color: "#24283b" - - // Required properties from parent - required property string activeView - required property int notificationCount - required property string currentDate - required property string currentTime - required property string weatherTemp - required property string weatherIcon - - // Signals to communicate with parent - signal toggleNotifications() - signal toggleCalendar() - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - - // Notification icon - Rectangle { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - radius: 6 - color: notifMouseArea.containsMouse ? "#33467c" : (statusBar.activeView === "notifications" ? "#33467c" : "transparent") - - Text { - anchors.centerIn: parent - text: "󰂚" - font.family: "JetBrains Mono" - font.pixelSize: 16 - color: statusBar.notificationCount > 0 ? "#ff9e64" : "#7aa2f7" - } - - MouseArea { - id: notifMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: statusBar.toggleNotifications() - } - } - - // Separator - Text { - text: "|" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#33467c" - } - - // System Tray - Row { - spacing: 4 - - Repeater { - model: SystemTray.items - - delegate: Rectangle { - id: trayItemRect - width: 24 - height: 24 - radius: 4 - color: trayMouseArea.containsMouse ? "#33467c" : "transparent" - - Image { - id: trayIcon - anchors.centerIn: parent - width: 18 - height: 18 - source: modelData.icon ?? "" - sourceSize: Qt.size(18, 18) - cache: false - visible: status === Image.Ready - } - - // Fallback when icon can't load - Text { - anchors.centerIn: parent - text: modelData.title ? modelData.title.charAt(0).toUpperCase() : "?" - font.family: "JetBrains Mono" - font.pixelSize: 12 - font.bold: true - color: "#c0caf5" - visible: trayIcon.status !== Image.Ready - } - - QsMenuAnchor { - id: trayMenuAnchor - menu: modelData.menu - anchor.item: trayItemRect - anchor.edges: Edges.Bottom - anchor.gravity: Edges.Bottom - } - - MouseArea { - id: trayMouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: (mouse) => { - if (mouse.button === Qt.RightButton || modelData.onlyMenu) { - if (modelData.hasMenu) { - trayMenuAnchor.open(); - } - } else if (mouse.button === Qt.MiddleButton) { - modelData.secondaryActivate(); - } else { - modelData.activate(); - } - } - onWheel: (wheel) => { - modelData.scroll(wheel.angleDelta.y / 120, false); - } - } - - ToolTip.visible: trayMouseArea.containsMouse && modelData.tooltipTitle - ToolTip.text: modelData.tooltipTitle || modelData.title - ToolTip.delay: 500 - } - } - } - - // Separator - Text { - text: "|" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#33467c" - } - - // Date/Time clickable area - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 28 - radius: 6 - color: dateTimeMouseArea.containsMouse ? "#33467c" : "transparent" - - MouseArea { - id: dateTimeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: statusBar.toggleCalendar() - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - spacing: 12 - - // Date - Text { - text: statusBar.currentDate - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - - Item { Layout.fillWidth: true } - - // Weather - Row { - spacing: 6 - visible: statusBar.weatherTemp !== "" - - Text { - text: statusBar.weatherIcon - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#7aa2f7" - } - - Text { - text: statusBar.weatherTemp - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - } - - // Separator - Text { - text: "|" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#33467c" - visible: statusBar.weatherTemp !== "" - } - - // Time - Text { - text: statusBar.currentTime - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - } - } - } -} diff --git a/dot_files/quickshell/defaults/config.json b/dot_files/quickshell/defaults/config.json new file mode 100644 index 0000000..cbdd354 --- /dev/null +++ b/dot_files/quickshell/defaults/config.json @@ -0,0 +1,44 @@ +{ + "appearance": { + "darkMode": true, + "accentColor": "blue", + "wallpaperTheming": false, + "panelOpacity": 0.85 + }, + "fonts": { + "family": "Inter", + "mono": "JetBrains Mono", + "size": 14 + }, + "bar": { + "showWeather": true, + "showBattery": true, + "showNetwork": true, + "showTray": true, + "showClock": true + }, + "notifications": { + "timeout": 5000, + "sounds": true, + "dndSchedule": false, + "dndStart": "22:00", + "dndEnd": "08:00" + }, + "sidebar": { + "animations": true, + "width": 380 + }, + "osd": { + "timeout": 1500, + "showValue": true + }, + "weather": { + "location": "", + "units": "metric", + "updateInterval": 900000 + }, + "launcher": { + "maxResults": 50, + "showCategories": true + } +} diff --git a/dot_files/quickshell/hud.md b/dot_files/quickshell/hud.md deleted file mode 100644 index 6728713..0000000 --- a/dot_files/quickshell/hud.md +++ /dev/null @@ -1,24 +0,0 @@ -# High Level Idea -We are going to create a single command panel that will start from our launcher base. -This means that to do anything on the system is only a single shortcut. - -When launched the command panel will overlay the screen with a blur and the content in a box in the center. - --------------------------------------------------------- -| date/time, weather, system tray, wifi, bluetooth, etc.. | --------------------------------------------------------- -| Perforamnce metrics cpu, ram, disk, temps, etc.. | --------------------------------------------------------- -| > Enter Command | --------------------------------------------------------- -| Command content goes here | --------------------------------------------------------- - -Now for example simply typing will launch an application -cmd+s | /[text] (3+ letters) will search users's files -cmd+n | /notifications or /n will show notifications -cmd+w | /weather will show the detailed weather for the week -cmd+p | /performance or /p will show more detailed performance metrics -cmd+b | /bluetooth or /b will show bluetooth controls (device pairing, etc..) -cmd+w | /wifi or /w will show wifi controls (status, connect, disconnect, etc..) -cmd+c | /calendar or /c will show a nice calendar widget diff --git a/dot_files/quickshell/hud/AppList.qml b/dot_files/quickshell/hud/AppList.qml deleted file mode 100644 index 3c27def..0000000 --- a/dot_files/quickshell/hud/AppList.qml +++ /dev/null @@ -1,118 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell - -Rectangle { - id: appListView - width: parent.width - height: visible ? Math.min(filteredApps.length * 52, 300) : 0 - color: "transparent" - - required property var filteredApps - required property bool isVisible - - visible: isVisible && filteredApps.length > 0 - - signal appLaunched(var app) - - property alias currentIndex: appList.currentIndex - - function moveUp() { - if (appList.currentIndex > 0) { - appList.currentIndex--; - } - } - - function moveDown() { - if (appList.currentIndex < filteredApps.length - 1) { - appList.currentIndex++; - } - } - - function launchCurrent() { - if (filteredApps.length > 0 && appList.currentIndex >= 0) { - appLaunched(filteredApps[appList.currentIndex]); - } - } - - ListView { - id: appList - anchors.fill: parent - clip: true - spacing: 4 - currentIndex: 0 - - model: appListView.filteredApps - - delegate: Rectangle { - required property var modelData - required property int index - - width: appList.width - height: 48 - radius: 6 - color: mouseArea.containsMouse || appList.currentIndex === index ? "#33467c" : "transparent" - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 12 - - // App icon - Image { - Layout.preferredWidth: 32 - Layout.preferredHeight: 32 - source: modelData.icon ? "image://icon/" + modelData.icon : "" - sourceSize: Qt.size(32, 32) - - Text { - anchors.centerIn: parent - text: "" - font.family: "JetBrains Mono" - font.pixelSize: 20 - color: "#7aa2f7" - visible: parent.status !== Image.Ready - } - } - - // App name and description - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - Layout.fillWidth: true - text: modelData.name - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - elide: Text.ElideRight - } - - Text { - Layout.fillWidth: true - text: modelData.genericName || "" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - elide: Text.ElideRight - visible: text !== "" - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - onClicked: appListView.appLaunched(modelData) - } - } - - ScrollBar.vertical: ScrollBar { - active: true - policy: ScrollBar.AsNeeded - } - } -} diff --git a/dot_files/quickshell/hud/BluetoothView.qml b/dot_files/quickshell/hud/BluetoothView.qml deleted file mode 100644 index 887a348..0000000 --- a/dot_files/quickshell/hud/BluetoothView.qml +++ /dev/null @@ -1,464 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell.Io - -Rectangle { - id: bluetoothView - width: parent.width - height: visible ? Math.min(bluetoothContent.implicitHeight, 400) : 0 - color: "transparent" - clip: true - - required property bool isVisible - visible: isVisible - - property bool btPowered: false - property bool btScanning: false - property var btDevices: [] - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - // Refresh bluetooth status when view becomes visible - onVisibleChanged: { - if (visible) { - btStatusProcess.running = true; - btDevicesProcess.running = true; - } - } - - // Get bluetooth power status - Process { - id: btStatusProcess - command: ["bluetoothctl", "show"] - stdout: SplitParser { - onRead: (line) => { - if (line.includes("Powered:")) { - bluetoothView.btPowered = line.includes("yes"); - } - if (line.includes("Discovering:")) { - bluetoothView.btScanning = line.includes("yes"); - } - } - } - } - - // Get paired/known devices - Process { - id: btDevicesProcess - command: ["bluetoothctl", "devices"] - property string output: "" - stdout: SplitParser { - onRead: (line) => { - btDevicesProcess.output += line + "\n"; - } - } - onRunningChanged: { - if (!running && output !== "") { - var devices = []; - var lines = output.trim().split("\n"); - for (var i = 0; i < lines.length; i++) { - var match = lines[i].match(/Device ([0-9A-F:]+) (.+)/); - if (match) { - devices.push({ - address: match[1], - name: match[2], - connected: false, - paired: false - }); - } - } - // Now get info for each device - if (devices.length > 0) { - bluetoothView.btDevices = devices; - btInfoProcess.deviceIndex = 0; - btInfoProcess.runNextDevice(); - } else { - bluetoothView.btDevices = []; - } - output = ""; - } - } - } - - // Get detailed info for each device - Process { - id: btInfoProcess - property int deviceIndex: 0 - property string currentOutput: "" - - function runNextDevice() { - if (deviceIndex < bluetoothView.btDevices.length) { - currentOutput = ""; - command = ["bluetoothctl", "info", bluetoothView.btDevices[deviceIndex].address]; - running = true; - } - } - - stdout: SplitParser { - onRead: (line) => { - btInfoProcess.currentOutput += line + "\n"; - } - } - - onRunningChanged: { - if (!running && currentOutput !== "") { - var devices = bluetoothView.btDevices.slice(); - var dev = devices[deviceIndex]; - if (currentOutput.includes("Connected: yes")) { - dev.connected = true; - } - if (currentOutput.includes("Paired: yes")) { - dev.paired = true; - } - if (currentOutput.includes("Icon:")) { - var iconMatch = currentOutput.match(/Icon: (.+)/); - if (iconMatch) dev.icon = iconMatch[1].trim(); - } - devices[deviceIndex] = dev; - bluetoothView.btDevices = devices; - - deviceIndex++; - runNextDevice(); - } - } - } - - // Power on/off - Process { - id: btPowerProcess - property bool targetState: false - command: ["bluetoothctl", "power", targetState ? "on" : "off"] - onRunningChanged: { - if (!running) { - btStatusProcess.running = true; - } - } - } - - // Scan on/off - Process { - id: btScanProcess - property bool targetState: false - command: ["bluetoothctl", "scan", targetState ? "on" : "off"] - onRunningChanged: { - if (!running) { - btStatusProcess.running = true; - if (!targetState) { - btDevicesProcess.running = true; - } - } - } - } - - // Connect to device - Process { - id: btConnectProcess - property string targetAddress: "" - command: ["bluetoothctl", "connect", targetAddress] - onRunningChanged: { - if (!running) { - btDevicesProcess.running = true; - } - } - } - - // Disconnect from device - Process { - id: btDisconnectProcess - property string targetAddress: "" - command: ["bluetoothctl", "disconnect", targetAddress] - onRunningChanged: { - if (!running) { - btDevicesProcess.running = true; - } - } - } - - // Background refresh timer - updates device list every 10 seconds while visible - Timer { - interval: 10000 - running: bluetoothView.visible && bluetoothView.btPowered - repeat: true - onTriggered: btDevicesProcess.running = true - } - - Column { - id: bluetoothContent - width: parent.width - spacing: 8 - - // Adapter controls - Rectangle { - width: parent.width - height: 44 - radius: 6 - color: "#24283b" - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - - // Bluetooth icon - Text { - text: "󰂯" - font.family: "JetBrains Mono" - font.pixelSize: 18 - color: bluetoothView.btPowered ? "#7aa2f7" : "#565f89" - } - - // Status text - Text { - text: bluetoothView.btPowered ? "Bluetooth On" : "Bluetooth Off" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - - Item { Layout.fillWidth: true } - - // Scan button - Rectangle { - Layout.preferredWidth: scanText.implicitWidth + 16 - Layout.preferredHeight: 28 - radius: 4 - color: scanMouse.containsMouse ? "#33467c" : "transparent" - border.color: bluetoothView.btScanning ? "#7aa2f7" : "#33467c" - border.width: 1 - visible: bluetoothView.btPowered - - Text { - id: scanText - anchors.centerIn: parent - text: bluetoothView.btScanning ? "󰍰 Scanning..." : "󰍉 Scan" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: bluetoothView.btScanning ? "#7aa2f7" : "#c0caf5" - } - - MouseArea { - id: scanMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - btScanProcess.targetState = !bluetoothView.btScanning; - btScanProcess.running = true; - } - } - } - - // Power toggle - Rectangle { - Layout.preferredWidth: 48 - Layout.preferredHeight: 28 - radius: 14 - color: bluetoothView.btPowered ? "#7aa2f7" : "#33467c" - - Rectangle { - width: 22 - height: 22 - radius: 11 - color: "#c0caf5" - anchors.verticalCenter: parent.verticalCenter - x: bluetoothView.btPowered ? parent.width - width - 3 : 3 - - Behavior on x { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - btPowerProcess.targetState = !bluetoothView.btPowered; - btPowerProcess.running = true; - } - } - } - } - } - - // Devices list header - Text { - text: "Devices" - font.family: "JetBrains Mono" - font.pixelSize: 12 - font.bold: true - color: "#565f89" - visible: bluetoothView.btDevices.length > 0 - } - - // Device list - ListView { - id: bluetoothDeviceList - width: parent.width - height: Math.min(contentHeight, 280) - clip: true - spacing: 4 - visible: bluetoothView.btDevices.length > 0 - - model: bluetoothView.btDevices - - delegate: Rectangle { - required property var modelData - required property int index - - width: bluetoothDeviceList.width - height: 56 - radius: 6 - color: btDeviceMouse.containsMouse ? "#33467c" : "#24283b" - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - - // Device icon - Text { - text: { - var icon = modelData.icon || ""; - if (icon.includes("phone")) return "󰏲"; - if (icon.includes("audio") || icon.includes("headset") || icon.includes("headphone")) return "󰋋"; - if (icon.includes("keyboard")) return "󰌌"; - if (icon.includes("mouse")) return "󰍽"; - if (icon.includes("computer")) return "󰍹"; - return "󰂱"; - } - font.family: "JetBrains Mono" - font.pixelSize: 20 - color: modelData.connected ? "#7aa2f7" : "#565f89" - } - - // Device info - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Text { - text: modelData.name || modelData.address - font.family: "JetBrains Mono" - font.pixelSize: 13 - color: "#c0caf5" - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: { - if (modelData.connected) return "Connected"; - if (modelData.paired) return "Paired"; - return "Available"; - } - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: modelData.connected ? "#7aa2f7" : "#565f89" - Layout.fillWidth: true - } - } - - // Connect/Disconnect button - Rectangle { - Layout.preferredWidth: connectBtnText.implicitWidth + 16 - Layout.preferredHeight: 28 - radius: 4 - color: connectBtnMouse.containsMouse ? (modelData.connected ? "#f7768e" : "#7aa2f7") : "transparent" - border.color: modelData.connected ? "#f7768e" : "#7aa2f7" - border.width: 1 - - Text { - id: connectBtnText - anchors.centerIn: parent - text: modelData.connected ? "Disconnect" : "Connect" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: connectBtnMouse.containsMouse ? "#1a1b26" : (modelData.connected ? "#f7768e" : "#7aa2f7") - } - - MouseArea { - id: connectBtnMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (modelData.connected) { - btDisconnectProcess.targetAddress = modelData.address; - btDisconnectProcess.running = true; - } else { - btConnectProcess.targetAddress = modelData.address; - btConnectProcess.running = true; - } - } - } - } - } - - MouseArea { - id: btDeviceMouse - anchors.fill: parent - hoverEnabled: true - z: -1 - } - } - - ScrollBar.vertical: ScrollBar { - active: true - policy: ScrollBar.AsNeeded - } - } - - // Empty state - Rectangle { - width: parent.width - height: 80 - radius: 6 - color: "#24283b" - visible: bluetoothView.btDevices.length === 0 && bluetoothView.btPowered - - Column { - anchors.centerIn: parent - spacing: 8 - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: bluetoothView.btScanning ? "Scanning for devices..." : "No devices found" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#565f89" - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "Click Scan to discover nearby devices" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - visible: !bluetoothView.btScanning - } - } - } - - // Adapter disabled state - Rectangle { - width: parent.width - height: 60 - radius: 6 - color: "#24283b" - visible: !bluetoothView.btPowered - - Text { - anchors.centerIn: parent - text: "Turn on Bluetooth to see devices" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#565f89" - } - } - } -} diff --git a/dot_files/quickshell/hud/CalendarView.qml b/dot_files/quickshell/hud/CalendarView.qml deleted file mode 100644 index 3137932..0000000 --- a/dot_files/quickshell/hud/CalendarView.qml +++ /dev/null @@ -1,189 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -Rectangle { - id: calendarView - width: parent.width - height: visible ? calendarContent.implicitHeight : 0 - color: "transparent" - clip: true - - required property bool isVisible - visible: isVisible - - property int calendarMonth: new Date().getMonth() - property int calendarYear: new Date().getFullYear() - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - function reset() { - var now = new Date(); - calendarMonth = now.getMonth(); - calendarYear = now.getFullYear(); - } - - Column { - id: calendarContent - width: parent.width - spacing: 8 - - // Month/Year header with navigation - Rectangle { - width: parent.width - height: 36 - radius: 6 - color: "#24283b" - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - - // Previous month - Rectangle { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - radius: 4 - color: prevMonthMouse.containsMouse ? "#33467c" : "transparent" - - Text { - anchors.centerIn: parent - text: "<" - font.family: "JetBrains Mono" - font.pixelSize: 16 - font.bold: true - color: "#c0caf5" - } - - MouseArea { - id: prevMonthMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (calendarView.calendarMonth === 0) { - calendarView.calendarMonth = 11; - calendarView.calendarYear--; - } else { - calendarView.calendarMonth--; - } - } - } - } - - Item { Layout.fillWidth: true } - - // Month Year display - Text { - text: new Date(calendarView.calendarYear, calendarView.calendarMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") - font.family: "JetBrains Mono" - font.pixelSize: 14 - font.bold: true - color: "#c0caf5" - } - - Item { Layout.fillWidth: true } - - // Next month - Rectangle { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - radius: 4 - color: nextMonthMouse.containsMouse ? "#33467c" : "transparent" - - Text { - anchors.centerIn: parent - text: ">" - font.family: "JetBrains Mono" - font.pixelSize: 16 - font.bold: true - color: "#c0caf5" - } - - MouseArea { - id: nextMonthMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - if (calendarView.calendarMonth === 11) { - calendarView.calendarMonth = 0; - calendarView.calendarYear++; - } else { - calendarView.calendarMonth++; - } - } - } - } - } - } - - // Day headers - Row { - width: parent.width - - Repeater { - model: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] - delegate: Item { - width: parent.width / 7 - height: 24 - - Text { - anchors.centerIn: parent - text: modelData - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - } - } - } - } - - // Calendar grid - Grid { - id: calendarGrid - width: parent.width - columns: 7 - - property var firstDay: new Date(calendarView.calendarYear, calendarView.calendarMonth, 1) - property int startDay: firstDay.getDay() - property int daysInMonth: new Date(calendarView.calendarYear, calendarView.calendarMonth + 1, 0).getDate() - property var today: new Date() - - Repeater { - model: 42 - - delegate: Item { - width: calendarGrid.width / 7 - height: 32 - - property int dayNum: index - calendarGrid.startDay + 1 - property bool isCurrentMonth: dayNum > 0 && dayNum <= calendarGrid.daysInMonth - property bool isToday: isCurrentMonth && - dayNum === calendarGrid.today.getDate() && - calendarView.calendarMonth === calendarGrid.today.getMonth() && - calendarView.calendarYear === calendarGrid.today.getFullYear() - - Rectangle { - anchors.centerIn: parent - width: 28 - height: 28 - radius: 14 - color: isToday ? "#7aa2f7" : "transparent" - visible: isCurrentMonth - - Text { - anchors.centerIn: parent - text: isCurrentMonth ? dayNum : "" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: isToday ? "#1a1b26" : "#c0caf5" - } - } - } - } - } - } -} diff --git a/dot_files/quickshell/hud/HelpView.qml b/dot_files/quickshell/hud/HelpView.qml deleted file mode 100644 index 450a628..0000000 --- a/dot_files/quickshell/hud/HelpView.qml +++ /dev/null @@ -1,166 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -Rectangle { - id: helpView - width: parent.width - height: visible ? helpContent.implicitHeight : 0 - color: "transparent" - clip: true - - required property bool isVisible - visible: isVisible - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - Column { - id: helpContent - width: parent.width - spacing: 8 - - // Commands section - Rectangle { - width: parent.width - height: commandsCol.implicitHeight + 16 - radius: 6 - color: "#24283b" - - Column { - id: commandsCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 6 - - Text { - text: "Commands" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#7aa2f7" - } - - // Command list - Repeater { - model: [ - { cmd: "/? or /help", desc: "Show this help" }, - { cmd: "/c or /calendar", desc: "Show calendar" }, - { cmd: "/n or /notifications", desc: "Show notifications" }, - { cmd: "/b or /bluetooth", desc: "Bluetooth controls" } - ] - - Row { - spacing: 12 - Text { - width: 140 - text: modelData.cmd - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#9ece6a" - } - Text { - text: modelData.desc - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#c0caf5" - } - } - } - } - } - - // Keyboard shortcuts section - Rectangle { - width: parent.width - height: shortcutsCol.implicitHeight + 16 - radius: 6 - color: "#24283b" - - Column { - id: shortcutsCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 6 - - Text { - text: "Keyboard Shortcuts" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#7aa2f7" - } - - Repeater { - model: [ - { key: "Ctrl+C", desc: "Open calendar" }, - { key: "Ctrl+N", desc: "Open notifications" }, - { key: "Ctrl+B", desc: "Open bluetooth" }, - { key: "Ctrl+J", desc: "Focus list (in notifications)" }, - { key: "Ctrl+K", desc: "Return to input" }, - { key: "Escape", desc: "Clear input / close HUD" }, - { key: "↑ / ↓", desc: "Navigate list" }, - { key: "j / k", desc: "Navigate list (vim)" }, - { key: "Enter", desc: "Launch app / confirm" }, - { key: "d / x", desc: "Dismiss notification" } - ] - - Row { - spacing: 12 - Text { - width: 140 - text: modelData.key - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#ff9e64" - } - Text { - text: modelData.desc - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#c0caf5" - } - } - } - } - } - - // Tips section - Rectangle { - width: parent.width - height: tipsCol.implicitHeight + 16 - radius: 6 - color: "#24283b" - - Column { - id: tipsCol - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 6 - - Text { - text: "Tips" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#7aa2f7" - } - - Text { - width: parent.width - text: "• Type to search applications\n• Click date/time to toggle calendar\n• Click bell icon to toggle notifications" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: "#c0caf5" - lineHeight: 1.4 - } - } - } - } -} diff --git a/dot_files/quickshell/hud/NotificationsView.qml b/dot_files/quickshell/hud/NotificationsView.qml deleted file mode 100644 index dfd5602..0000000 --- a/dot_files/quickshell/hud/NotificationsView.qml +++ /dev/null @@ -1,331 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls - -Rectangle { - id: notificationsView - width: parent.width - height: visible ? Math.min(notificationsContent.implicitHeight, 350) : 0 - color: "transparent" - clip: true - - required property bool isVisible - required property var notificationList - - visible: isVisible - - signal dismissNotification(var notification) - signal clearAllNotifications() - signal requestFocusInput() - - property alias currentIndex: notificationListView.currentIndex - property bool hasSelection: false - property bool clearAllSelected: false - property int lastSelectedIndex: 0 - - Behavior on height { - NumberAnimation { duration: 150; easing.type: Easing.OutQuad } - } - - function focusList() { - if (notificationList.length > 0) { - hasSelection = true; - clearAllSelected = false; - // Restore last selected, clamped to valid range - var idx = Math.min(lastSelectedIndex, notificationList.length - 1); - notificationListView.currentIndex = idx; - notificationListView.positionViewAtIndex(idx, ListView.Contain); - notificationListView.forceActiveFocus(); - } - } - - function selectAndScroll(newIndex) { - if (newIndex >= 0 && newIndex < notificationListView.count) { - hasSelection = true; - clearAllSelected = false; - lastSelectedIndex = newIndex; - notificationListView.currentIndex = newIndex; - notificationListView.positionViewAtIndex(newIndex, ListView.Contain); - } - } - - function selectClearAll() { - hasSelection = false; - clearAllSelected = true; - } - - function moveUp() { - if (clearAllSelected) { - // Move from Clear All back to last notification - clearAllSelected = false; - hasSelection = true; - notificationListView.currentIndex = notificationListView.count - 1; - notificationListView.positionViewAtIndex(notificationListView.currentIndex, ListView.Contain); - } else if (!hasSelection) { - selectAndScroll(0); - } else if (currentIndex > 0) { - selectAndScroll(currentIndex - 1); - } - } - - function moveDown() { - if (clearAllSelected) { - // Already at bottom, do nothing - return; - } else if (!hasSelection) { - selectAndScroll(0); - } else if (currentIndex < notificationListView.count - 1) { - selectAndScroll(currentIndex + 1); - } else if (currentIndex === notificationListView.count - 1) { - // At last notification, move to Clear All - selectClearAll(); - } - } - - function exitUp() { - // Ctrl+K - exit up - if (clearAllSelected) { - // From Clear All, go to bottom of list - clearAllSelected = false; - hasSelection = true; - notificationListView.currentIndex = notificationListView.count - 1; - notificationListView.positionViewAtIndex(notificationListView.currentIndex, ListView.Contain); - } else { - // From list, go to text box - hasSelection = false; - clearAllSelected = false; - requestFocusInput(); - } - } - - function exitDown() { - // Ctrl+J - exit to Clear All - hasSelection = false; - selectClearAll(); - } - - function dismissCurrentAndSelectNext() { - if (!hasSelection || currentIndex < 0 || currentIndex >= notificationList.length) { - return; - } - var dismissIdx = currentIndex; - var nextIdx = dismissIdx; - // If we're at the last item, select the previous one - if (dismissIdx >= notificationList.length - 1) { - nextIdx = Math.max(0, dismissIdx - 1); - } - lastSelectedIndex = nextIdx; - var notif = notificationList[dismissIdx]; - dismissNotification(notif); - // After dismiss, list will be one shorter - update current index - // Use Qt.callLater to ensure list has updated first - Qt.callLater(function() { - if (notificationList.length > 0) { - var clampedIdx = Math.min(nextIdx, notificationList.length - 1); - notificationListView.currentIndex = clampedIdx; - lastSelectedIndex = clampedIdx; - notificationListView.positionViewAtIndex(clampedIdx, ListView.Contain); - } else { - hasSelection = false; - } - }); - } - - Column { - id: notificationsContent - width: parent.width - spacing: 4 - - // Empty state - Rectangle { - width: parent.width - height: 60 - radius: 6 - color: "#24283b" - visible: notificationsView.notificationList.length === 0 - - Text { - anchors.centerIn: parent - text: "No notifications" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#565f89" - } - } - - // Notification list - ListView { - id: notificationListView - width: parent.width - height: Math.min(contentHeight, 300) - clip: true - spacing: 4 - visible: notificationsView.notificationList.length > 0 - highlightFollowsCurrentItem: false - highlight: null - - model: notificationsView.notificationList - - delegate: Rectangle { - required property var modelData - required property int index - - width: notificationListView.width - height: notifContent.implicitHeight + 16 - radius: 6 - color: notifMouse.containsMouse || (notificationsView.hasSelection && notificationListView.currentIndex === index) ? "#33467c" : "#24283b" - - Column { - id: notifContent - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 8 - spacing: 4 - - RowLayout { - width: parent.width - spacing: 8 - - Text { - text: modelData.summary || modelData.appName || "Notification" - font.family: "JetBrains Mono" - font.pixelSize: 13 - font.bold: true - color: "#c0caf5" - elide: Text.ElideRight - Layout.fillWidth: true - } - - Text { - text: "" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: dismissNotifMouse.containsMouse ? "#f7768e" : "#565f89" - - MouseArea { - id: dismissNotifMouse - anchors.fill: parent - anchors.margins: -4 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: notificationsView.dismissNotification(modelData) - } - } - } - - Text { - width: parent.width - text: modelData.body || "" - font.family: "JetBrains Mono" - font.pixelSize: 11 - color: "#565f89" - wrapMode: Text.WordWrap - maximumLineCount: 2 - elide: Text.ElideRight - visible: text !== "" - } - } - - MouseArea { - id: notifMouse - anchors.fill: parent - hoverEnabled: true - z: -1 - onClicked: { - notificationsView.hasSelection = true; - notificationListView.currentIndex = index; - } - } - } - - // Keyboard navigation - Keys.onUpPressed: notificationsView.moveUp() - Keys.onDownPressed: notificationsView.moveDown() - Keys.onReturnPressed: { - if (notificationsView.clearAllSelected) { - notificationsView.clearAllNotifications(); - } else if (notificationsView.hasSelection && currentIndex >= 0 && currentIndex < notificationsView.notificationList.length) { - // Invoke default action (first action) on Enter - var notif = notificationsView.notificationList[currentIndex]; - if (notif.actions && notif.actions.length > 0) { - notif.actions[0].invoke(); - // If not resident, notification auto-dismisses, select next - if (!notif.resident) { - notificationsView.dismissCurrentAndSelectNext(); - } - } - } - } - Keys.onPressed: (event) => { - if (event.modifiers & Qt.ControlModifier) { - // Ctrl+K - exit up to text box - if (event.key === Qt.Key_K) { - notificationsView.exitUp(); - event.accepted = true; - } - // Ctrl+J - exit down to Clear All - else if (event.key === Qt.Key_J) { - notificationsView.exitDown(); - event.accepted = true; - } - } else { - // Vim bindings (no modifier) - if (event.key === Qt.Key_K) { - notificationsView.moveUp(); - event.accepted = true; - } else if (event.key === Qt.Key_J) { - notificationsView.moveDown(); - event.accepted = true; - } else if (event.key === Qt.Key_G && (event.modifiers & Qt.ShiftModifier)) { - // Shift+G - go to last notification - notificationsView.selectAndScroll(notificationListView.count - 1); - event.accepted = true; - } else if (event.key === Qt.Key_G) { - // g - go to top - notificationsView.selectAndScroll(0); - event.accepted = true; - } else if (event.key === Qt.Key_D || event.key === Qt.Key_X || - event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { - // Dismiss current and select next - notificationsView.dismissCurrentAndSelectNext(); - event.accepted = true; - } else if (event.key === Qt.Key_Escape) { - notificationsView.exitUp(); - event.accepted = true; - } - } - } - - ScrollBar.vertical: ScrollBar { - active: true - policy: ScrollBar.AsNeeded - } - } - - // Clear all button (at bottom) - Rectangle { - width: parent.width - height: 28 - radius: 6 - color: clearAllMouse.containsMouse || notificationsView.clearAllSelected ? "#f7768e" : "#24283b" - visible: notificationsView.notificationList.length > 0 - - Text { - anchors.centerIn: parent - text: " Clear All (" + notificationsView.notificationList.length + ")" - font.family: "JetBrains Mono" - font.pixelSize: 12 - color: clearAllMouse.containsMouse || notificationsView.clearAllSelected ? "#1a1b26" : "#565f89" - } - - MouseArea { - id: clearAllMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: notificationsView.clearAllNotifications() - } - } - } -} diff --git a/dot_files/quickshell/hud/StatusBar.qml b/dot_files/quickshell/hud/StatusBar.qml deleted file mode 100644 index b0e6a5c..0000000 --- a/dot_files/quickshell/hud/StatusBar.qml +++ /dev/null @@ -1,214 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import Quickshell -import Quickshell.Services.SystemTray - -Rectangle { - id: statusBar - width: parent.width - height: 40 - radius: 8 - color: "#24283b" - - // Required properties from parent - required property string activeView - required property int notificationCount - required property string currentDate - required property string currentTime - required property string weatherTemp - required property string weatherIcon - - // Signals to communicate with parent - signal toggleNotifications() - signal toggleCalendar() - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.rightMargin: 12 - spacing: 12 - - // Notification icon - Rectangle { - Layout.preferredWidth: 28 - Layout.preferredHeight: 28 - radius: 6 - color: notifMouseArea.containsMouse ? "#33467c" : (statusBar.activeView === "notifications" ? "#33467c" : "transparent") - - Text { - anchors.centerIn: parent - text: "󰂚" - font.family: "JetBrains Mono" - font.pixelSize: 16 - color: statusBar.notificationCount > 0 ? "#ff9e64" : "#7aa2f7" - } - - MouseArea { - id: notifMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: statusBar.toggleNotifications() - } - } - - // Separator - Text { - text: "|" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#33467c" - } - - // System Tray - Row { - spacing: 4 - - Repeater { - model: SystemTray.items - - delegate: Rectangle { - id: trayItemRect - width: 24 - height: 24 - radius: 4 - color: trayMouseArea.containsMouse ? "#33467c" : "transparent" - - Image { - id: trayIcon - anchors.centerIn: parent - width: 18 - height: 18 - source: modelData.icon ?? "" - sourceSize: Qt.size(18, 18) - cache: false - visible: status === Image.Ready - } - - // Fallback when icon can't load - Text { - anchors.centerIn: parent - text: modelData.title ? modelData.title.charAt(0).toUpperCase() : "?" - font.family: "JetBrains Mono" - font.pixelSize: 12 - font.bold: true - color: "#c0caf5" - visible: trayIcon.status !== Image.Ready - } - - QsMenuAnchor { - id: trayMenuAnchor - menu: modelData.menu - anchor.item: trayItemRect - anchor.edges: Edges.Bottom - anchor.gravity: Edges.Bottom - } - - MouseArea { - id: trayMouseArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: (mouse) => { - if (mouse.button === Qt.RightButton || modelData.onlyMenu) { - if (modelData.hasMenu) { - trayMenuAnchor.open(); - } - } else if (mouse.button === Qt.MiddleButton) { - modelData.secondaryActivate(); - } else { - modelData.activate(); - } - } - onWheel: (wheel) => { - modelData.scroll(wheel.angleDelta.y / 120, false); - } - } - - ToolTip.visible: trayMouseArea.containsMouse && modelData.tooltipTitle - ToolTip.text: modelData.tooltipTitle || modelData.title - ToolTip.delay: 500 - } - } - } - - // Separator - Text { - text: "|" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#33467c" - } - - // Date/Time clickable area - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 28 - radius: 6 - color: dateTimeMouseArea.containsMouse ? "#33467c" : "transparent" - - MouseArea { - id: dateTimeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: statusBar.toggleCalendar() - } - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - spacing: 12 - - // Date - Text { - text: statusBar.currentDate - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - - Item { Layout.fillWidth: true } - - // Weather - Row { - spacing: 6 - visible: statusBar.weatherTemp !== "" - - Text { - text: statusBar.weatherIcon - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#7aa2f7" - } - - Text { - text: statusBar.weatherTemp - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - } - - // Separator - Text { - text: "|" - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#33467c" - visible: statusBar.weatherTemp !== "" - } - - // Time - Text { - text: statusBar.currentTime - font.family: "JetBrains Mono" - font.pixelSize: 14 - color: "#c0caf5" - } - } - } - } -} diff --git a/dot_files/quickshell/modules/bar/StatusBar.qml b/dot_files/quickshell/modules/bar/StatusBar.qml new file mode 100644 index 0000000..01577aa --- /dev/null +++ b/dot_files/quickshell/modules/bar/StatusBar.qml @@ -0,0 +1,294 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +import "../common" as Common +import "../../services" as Services +import "../../" as Root + +PanelWindow { + id: root + + required property var targetScreen + screen: targetScreen + + anchors { + top: true + left: true + right: true + } + + implicitHeight: Common.Appearance.sizes.barHeight + color: "transparent" + + // Bar should be above click catchers so it's always clickable + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "statusbar" + + // Bar button component + component BarButton: MouseArea { + id: button + + property string icon: "" + property string buttonText: "" + property string tooltip: "" + property bool highlighted: false + property color textColor: Common.Appearance.m3colors.onSurface + + Layout.preferredHeight: 28 + // Icon-only buttons get minimal padding, buttons with text get more + Layout.preferredWidth: button.buttonText === "" + ? 28 + : buttonContent.implicitWidth + Common.Appearance.spacing.small * 2 + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: button.highlighted + ? Common.Appearance.m3colors.primaryContainer + : (button.containsMouse + ? Common.Appearance.m3colors.surfaceVariant + : "transparent") + + Behavior on color { + ColorAnimation { duration: 150 } + } + } + + RowLayout { + id: buttonContent + anchors.centerIn: parent + spacing: Common.Appearance.spacing.tiny + + Text { + visible: button.icon !== "" + text: button.icon + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: button.highlighted + ? Common.Appearance.m3colors.onPrimaryContainer + : button.textColor + } + + Text { + visible: button.buttonText !== "" + text: button.buttonText + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: button.highlighted + ? Common.Appearance.m3colors.onPrimaryContainer + : button.textColor + } + } + } + + // Bar indicator (icon only, no interaction) + component BarIndicator: Item { + property string icon: "" + property string tooltip: "" + property color iconColor: Common.Appearance.m3colors.onSurface + + Layout.preferredHeight: 28 + Layout.preferredWidth: 28 + + Text { + anchors.centerIn: parent + text: parent.icon + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: parent.iconColor + } + } + + // Bar background + Rectangle { + anchors.fill: parent + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + Common.Appearance.panelOpacity + ) + + } + + // Helper properties for screen position (reactive to screen changes) + property bool isLeftmost: { + // Single monitor case + if (Quickshell.screens.length === 1) return true + // Multi-monitor: check against leftmost screen + return targetScreen === Root.GlobalStates.leftmostScreen + } + property bool isRightmost: { + // Single monitor case + if (Quickshell.screens.length === 1) return true + // Multi-monitor: check against rightmost screen + return targetScreen === Root.GlobalStates.rightmostScreen + } + + // Bar content + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.medium + anchors.rightMargin: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.small + + // Left section - Launcher button (only on leftmost screen) + BarButton { + visible: root.isLeftmost + icon: Common.Icons.icons.apps + tooltip: "Applications" + onClicked: Root.GlobalStates.toggleSidebarLeft(root.targetScreen) + } + + // Spacer + Item { Layout.fillWidth: true } + + // Right section - System indicators (only on rightmost screen) + RowLayout { + visible: root.isRightmost + spacing: 2 + + // Weather (if enabled) + BarButton { + visible: Common.Config.showWeather && Services.Weather.ready + icon: Common.Icons.weatherIcon(Services.Weather.condition, Services.Weather.isNight) + buttonText: Services.Weather.temperature + tooltip: Services.Weather.description + } + + // Privacy indicators + BarButton { + visible: Services.Privacy.cameraInUse + icon: Common.Icons.icons.camera + textColor: Common.Appearance.m3colors.error + tooltip: "Camera in use" + } + + // Network - only show if wifi available (for wifi) or ethernet connected + BarIndicator { + visible: Common.Config.showNetwork && (Services.Network.wifiAvailable || (Services.Network.connected && Services.Network.type === "ethernet")) + icon: { + if (!Services.Network.connected) { + return Services.Network.wifiAvailable ? Common.Icons.icons.wifiOff : Common.Icons.icons.ethernetOff + } + if (Services.Network.type === "wifi") { + return Common.Icons.wifiIcon(Services.Network.strength, true) + } + return Common.Icons.icons.ethernet + } + tooltip: Services.Network.connected + ? Services.Network.name + : "Disconnected" + } + + // Audio output + BarButton { + icon: Services.Audio.muted + ? Common.Icons.icons.volumeOff + : Common.Icons.volumeIcon(Services.Audio.volume * 100, false) + tooltip: Services.Audio.muted + ? "Volume: Muted" + : "Volume: " + Math.round(Services.Audio.volume * 100) + "%" + textColor: Services.Audio.muted + ? Common.Appearance.m3colors.onSurfaceVariant + : Common.Appearance.m3colors.onSurface + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "audio") + } + + // Microphone + BarButton { + icon: Services.Audio.micMuted + ? Common.Icons.icons.micOff + : Common.Icons.icons.mic + tooltip: { + if (Services.Audio.micMuted && Services.Privacy.micInUse) { + return "Microphone: Muted (in use)" + } else if (Services.Audio.micMuted) { + return "Microphone: Muted" + } else if (Services.Privacy.micInUse) { + return "Microphone: In use" + } else { + return "Microphone: " + Math.round(Services.Audio.micVolume * 100) + "%" + } + } + textColor: Services.Privacy.micInUse + ? Common.Appearance.m3colors.error + : Common.Appearance.m3colors.onSurfaceVariant + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "audio") + } + + // Bluetooth + BarButton { + visible: Services.BluetoothStatus.available + icon: Services.BluetoothStatus.powered + ? (Services.BluetoothStatus.connected + ? Common.Icons.icons.bluetoothConnected + : Common.Icons.icons.bluetooth) + : Common.Icons.icons.bluetoothOff + tooltip: Services.BluetoothStatus.powered + ? (Services.BluetoothStatus.connected + ? "Bluetooth: " + Services.BluetoothStatus.connectedDeviceName + : "Bluetooth: On") + : "Bluetooth: Off" + textColor: Services.BluetoothStatus.connected + ? Common.Appearance.m3colors.primary + : (Services.BluetoothStatus.powered + ? Common.Appearance.m3colors.onSurface + : Common.Appearance.m3colors.onSurfaceVariant) + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "bluetooth") + } + + // Battery (if present) + BarButton { + visible: Common.Config.showBattery && Services.Battery.present + icon: Common.Icons.batteryIcon(Services.Battery.percent, Services.Battery.charging) + buttonText: Services.Battery.percent + "%" + tooltip: Services.Battery.charging + ? "Charging: " + Services.Battery.percent + "%" + : "Battery: " + Services.Battery.percent + "%" + textColor: Services.Battery.percent <= 20 && !Services.Battery.charging + ? Common.Appearance.m3colors.error + : Common.Appearance.m3colors.onSurface + } + + // Notifications bell + BarButton { + icon: Root.GlobalStates.doNotDisturb + ? Common.Icons.icons.doNotDisturb + : Common.Icons.icons.notification + tooltip: Root.GlobalStates.doNotDisturb + ? "Do Not Disturb" + : (Root.GlobalStates.unreadNotificationCount > 0 + ? Root.GlobalStates.unreadNotificationCount + " notifications" + : "Notifications") + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "notifications") + textColor: Root.GlobalStates.doNotDisturb + ? Common.Appearance.m3colors.onSurfaceVariant + : (Root.GlobalStates.unreadNotificationCount > 0 + ? Common.Appearance.m3colors.onSurface + : Common.Appearance.m3colors.onSurfaceVariant) + } + + // Date and Time - rightmost item + BarButton { + id: clockButton + buttonText: Services.DateTime.dateString + " " + Services.DateTime.timeString + tooltip: "Click to open calendar" + onClicked: Root.GlobalStates.toggleSidebarRight(root.targetScreen, "calendar") + + Timer { + interval: 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: Services.DateTime.update() + } + } + } + } +} diff --git a/dot_files/quickshell/modules/bar/qmldir b/dot_files/quickshell/modules/bar/qmldir new file mode 100644 index 0000000..1e891f5 --- /dev/null +++ b/dot_files/quickshell/modules/bar/qmldir @@ -0,0 +1,3 @@ +module bar + +StatusBar 1.0 StatusBar.qml diff --git a/dot_files/quickshell/modules/common/Appearance.qml b/dot_files/quickshell/modules/common/Appearance.qml new file mode 100644 index 0000000..fe11695 --- /dev/null +++ b/dot_files/quickshell/modules/common/Appearance.qml @@ -0,0 +1,222 @@ +pragma Singleton + +import QtQuick + +// Material Design 3 theming system with Tokyonight defaults +QtObject { + id: root + + // Dark mode toggle + property bool darkMode: true + + // Tokyonight color palette mapped to Material Design 3 roles + readonly property var m3colors: darkMode ? darkPalette : lightPalette + + readonly property var darkPalette: ({ + // Primary colors (Tokyonight blue) + primary: "#7aa2f7", + onPrimary: "#1a1b26", + primaryContainer: "#3d59a1", + onPrimaryContainer: "#c0caf5", + + // Secondary colors (Tokyonight green) + secondary: "#9ece6a", + onSecondary: "#1a1b26", + secondaryContainer: "#4a5e3a", + onSecondaryContainer: "#c0caf5", + + // Tertiary colors (Tokyonight purple) + tertiary: "#bb9af7", + onTertiary: "#1a1b26", + tertiaryContainer: "#6a4c93", + onTertiaryContainer: "#c0caf5", + + // Error colors (Tokyonight red) + error: "#f7768e", + onError: "#1a1b26", + errorContainer: "#8c4351", + onErrorContainer: "#ffc0c8", + + // Background and surface + background: "#1a1b26", + onBackground: "#c0caf5", + surface: "#1a1b26", + onSurface: "#c0caf5", + surfaceVariant: "#24283b", + onSurfaceVariant: "#a9b1d6", + + // Outline + outline: "#33467c", + outlineVariant: "#292e42", + + // Inverse + inverseSurface: "#c0caf5", + inverseOnSurface: "#1a1b26", + inversePrimary: "#3d59a1", + + // Additional Tokyonight colors + cyan: "#7dcfff", + orange: "#ff9e64", + yellow: "#e0af68", + magenta: "#ff007c", + teal: "#1abc9c", + comment: "#565f89" + }) + + readonly property var lightPalette: ({ + // Primary colors + primary: "#3d59a1", + onPrimary: "#ffffff", + primaryContainer: "#d0e4ff", + onPrimaryContainer: "#001d36", + + // Secondary colors + secondary: "#4a5e3a", + onSecondary: "#ffffff", + secondaryContainer: "#cce8b5", + onSecondaryContainer: "#0e2000", + + // Tertiary colors + tertiary: "#6a4c93", + onTertiary: "#ffffff", + tertiaryContainer: "#eddcff", + onTertiaryContainer: "#25005a", + + // Error colors + error: "#ba1a1a", + onError: "#ffffff", + errorContainer: "#ffdad6", + onErrorContainer: "#410002", + + // Background and surface + background: "#d5d6db", + onBackground: "#343338", + surface: "#f8f9ff", + onSurface: "#1a1b26", + surfaceVariant: "#e0e2ec", + onSurfaceVariant: "#44464f", + + // Outline + outline: "#74777f", + outlineVariant: "#c4c6d0", + + // Inverse + inverseSurface: "#2f3033", + inverseOnSurface: "#f1f0f4", + inversePrimary: "#9ecaff", + + // Additional colors + cyan: "#0891b2", + orange: "#c2410c", + yellow: "#a16207", + magenta: "#be185d", + teal: "#0d9488", + comment: "#6b7280" + }) + + // Surface layers (MD3 elevation) + function surfaceLayer(level: int): color { + const base = m3colors.surface + const tint = m3colors.primary + const alphas = [0, 0.05, 0.08, 0.11, 0.12, 0.14] + const alpha = alphas[Math.min(level, 5)] + return Qt.tint(base, Qt.rgba( + parseInt(tint.slice(1, 3), 16) / 255, + parseInt(tint.slice(3, 5), 16) / 255, + parseInt(tint.slice(5, 7), 16) / 255, + alpha + )) + } + + // Animation durations + readonly property var animation: ({ + // Expressive animations (spatial) + expressiveFast: 200, + expressive: 350, + expressiveSlow: 500, + + // Emphasized animations + emphasized: 500, + emphasizedAccel: 200, + emphasizedDecel: 400, + + // Standard animations + standard: 300, + standardAccel: 200, + standardDecel: 300 + }) + + // Easing curves + readonly property var easing: ({ + emphasized: Easing.BezierSpline, + emphasizedParams: [0.2, 0, 0, 1], + standard: Easing.OutCubic, + decelerate: Easing.OutQuart, + accelerate: Easing.InQuart + }) + + // Typography + readonly property var fonts: ({ + main: "Inter", + title: "Inter", + mono: "JetBrains Mono", + icon: "Symbols Nerd Font Mono" + }) + + readonly property var fontSize: ({ + smallest: 10, + small: 12, + normal: 14, + large: 16, + title: 20, + headline: 24, + display: 32 + }) + + // Spacing and rounding + readonly property var spacing: ({ + tiny: 4, + small: 8, + medium: 12, + large: 16, + xlarge: 24, + xxlarge: 32 + }) + + readonly property var rounding: ({ + none: 0, + small: 4, + medium: 8, + large: 12, + xlarge: 16, + full: 9999 + }) + + // Component sizes + readonly property var sizes: ({ + barHeight: 36, + sidebarWidth: 380, + osdWidth: 300, + osdHeight: 48, + launcherWidth: 600, + launcherHeight: 500, + notificationWidth: 380, + iconSmall: 16, + iconMedium: 20, + iconLarge: 24, + iconXLarge: 32 + }) + + // Transparency settings + property real panelOpacity: 0.85 + property real overlayOpacity: 0.95 + + // Helper function to get contrasting text color + function contrastText(backgroundColor: color): color { + const r = backgroundColor.r + const g = backgroundColor.g + const b = backgroundColor.b + const luminance = 0.299 * r + 0.587 * g + 0.114 * b + return luminance > 0.5 ? m3colors.onBackground : m3colors.background + } +} diff --git a/dot_files/quickshell/modules/common/ClickCatcher.qml b/dot_files/quickshell/modules/common/ClickCatcher.qml new file mode 100644 index 0000000..7cd2cca --- /dev/null +++ b/dot_files/quickshell/modules/common/ClickCatcher.qml @@ -0,0 +1,34 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland + +// Fullscreen transparent overlay that catches clicks outside panels +// Used to implement click-outside-to-close behavior +PanelWindow { + id: root + + required property var targetScreen + screen: targetScreen + + // Fill the entire screen + anchors { + top: true + bottom: true + left: true + right: true + } + + color: "transparent" + + // Must be below the actual panels in the layer stack + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "clickcatcher" + + // Signal emitted when the catcher is clicked + signal clicked() + + MouseArea { + anchors.fill: parent + onClicked: root.clicked() + } +} diff --git a/dot_files/quickshell/modules/common/Config.qml b/dot_files/quickshell/modules/common/Config.qml new file mode 100644 index 0000000..a64187b --- /dev/null +++ b/dot_files/quickshell/modules/common/Config.qml @@ -0,0 +1,253 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Persistent JSON configuration system +Singleton { + id: root + + // Configuration ready flag + property bool ready: false + + // Configuration file path + readonly property string configPath: Quickshell.env("XDG_CONFIG_HOME") + "/hypercube/shell.json" + readonly property string defaultConfigPath: Qt.resolvedUrl("../../defaults/config.json") + + // Appearance settings + property bool darkMode: true + property string accentColor: "blue" // blue, green, purple, orange, red, cyan + property bool wallpaperTheming: false + property real panelOpacity: 0.85 + + // Font settings + property string fontFamily: "Inter" + property string monoFontFamily: "JetBrains Mono" + property int fontSize: 14 + + // Bar settings + property bool showWeather: true + property bool showBattery: true + property bool showNetwork: true + property bool showTray: true + property bool showClock: true + + // Notification settings + property int notificationTimeout: 5000 + property bool notificationSounds: true + property bool doNotDisturbSchedule: false + property string doNotDisturbStart: "22:00" + property string doNotDisturbEnd: "08:00" + + // Sidebar settings + property bool sidebarAnimations: true + property int sidebarWidth: 380 + + // OSD settings + property int osdTimeout: 1500 + property bool osdShowValue: true + + // Weather settings + property string weatherLocation: "" // Empty = auto-detect + property string weatherUnits: "metric" // metric, imperial + property int weatherUpdateInterval: 900000 // 15 minutes + + // Launcher settings + property int launcherMaxResults: 50 + property bool launcherShowCategories: true + + // Check if config file exists and create if needed + Process { + id: configDirCheck + command: ["sh", "-c", "mkdir -p \"$(dirname '" + root.configPath + "')\" && [ -f '" + root.configPath + "' ] && echo exists || echo missing"] + running: true + onExited: { + if (stdout && stdout.trim() === "missing") { + console.log("Config: No user config found, using defaults") + root.ready = true + } else { + // File exists, load it + configFile.reload() + } + } + } + + // File watcher for config changes + FileView { + id: configFile + path: root.configPath + watchChanges: true + blockLoading: true + preload: false // Don't preload - we check existence first + + onFileChanged: { + reload() + } + + onLoaded: { + if (text()) { + root.parseConfig(text()) + } else { + root.ready = true + } + } + } + + // Parse configuration from JSON + function parseConfig(content) { + if (!content || content.trim() === "") { + console.log("Config: Empty config, using defaults") + ready = true + return + } + + try { + const config = JSON.parse(content) + + // Appearance + if (config.appearance) { + darkMode = config.appearance.darkMode ?? darkMode + accentColor = config.appearance.accentColor ?? accentColor + wallpaperTheming = config.appearance.wallpaperTheming ?? wallpaperTheming + panelOpacity = config.appearance.panelOpacity ?? panelOpacity + } + + // Fonts + if (config.fonts) { + fontFamily = config.fonts.family ?? fontFamily + monoFontFamily = config.fonts.mono ?? monoFontFamily + fontSize = config.fonts.size ?? fontSize + } + + // Bar + if (config.bar) { + showWeather = config.bar.showWeather ?? showWeather + showBattery = config.bar.showBattery ?? showBattery + showNetwork = config.bar.showNetwork ?? showNetwork + showTray = config.bar.showTray ?? showTray + showClock = config.bar.showClock ?? showClock + } + + // Notifications + if (config.notifications) { + notificationTimeout = config.notifications.timeout ?? notificationTimeout + notificationSounds = config.notifications.sounds ?? notificationSounds + doNotDisturbSchedule = config.notifications.dndSchedule ?? doNotDisturbSchedule + doNotDisturbStart = config.notifications.dndStart ?? doNotDisturbStart + doNotDisturbEnd = config.notifications.dndEnd ?? doNotDisturbEnd + } + + // Sidebar + if (config.sidebar) { + sidebarAnimations = config.sidebar.animations ?? sidebarAnimations + sidebarWidth = config.sidebar.width ?? sidebarWidth + } + + // OSD + if (config.osd) { + osdTimeout = config.osd.timeout ?? osdTimeout + osdShowValue = config.osd.showValue ?? osdShowValue + } + + // Weather + if (config.weather) { + weatherLocation = config.weather.location ?? weatherLocation + weatherUnits = config.weather.units ?? weatherUnits + weatherUpdateInterval = config.weather.updateInterval ?? weatherUpdateInterval + } + + // Launcher + if (config.launcher) { + launcherMaxResults = config.launcher.maxResults ?? launcherMaxResults + launcherShowCategories = config.launcher.showCategories ?? launcherShowCategories + } + + console.log("Config: Loaded successfully") + ready = true + } catch (e) { + console.error("Config: Failed to parse config:", e) + ready = true // Use defaults + } + } + + // Save configuration to file + function save() { + const config = { + appearance: { + darkMode: darkMode, + accentColor: accentColor, + wallpaperTheming: wallpaperTheming, + panelOpacity: panelOpacity + }, + fonts: { + family: fontFamily, + mono: monoFontFamily, + size: fontSize + }, + bar: { + showWeather: showWeather, + showBattery: showBattery, + showNetwork: showNetwork, + showTray: showTray, + showClock: showClock + }, + notifications: { + timeout: notificationTimeout, + sounds: notificationSounds, + dndSchedule: doNotDisturbSchedule, + dndStart: doNotDisturbStart, + dndEnd: doNotDisturbEnd + }, + sidebar: { + animations: sidebarAnimations, + width: sidebarWidth + }, + osd: { + timeout: osdTimeout, + showValue: osdShowValue + }, + weather: { + location: weatherLocation, + units: weatherUnits, + updateInterval: weatherUpdateInterval + }, + launcher: { + maxResults: launcherMaxResults, + showCategories: launcherShowCategories + } + } + + configFile.setText(JSON.stringify(config, null, 2)) + } + + // Load configuration + function load() { + // Config file will be loaded automatically via preload + // If already loaded, parse immediately + if (configFile.loaded) { + const content = configFile.text() + if (content && content.trim() !== "") { + parseConfig(content) + } else { + console.log("Config: No config file found, using defaults") + ready = true + } + } else { + // Will be parsed when onLoaded fires + console.log("Config: Waiting for config file to load...") + } + } + + // Set a nested value and save + function setValue(key, value) { + const parts = key.split(".") + if (parts.length === 1) { + root[key] = value + } else { + // Handle nested keys like "appearance.darkMode" + root[parts[1]] = value + } + save() + } +} diff --git a/dot_files/quickshell/modules/common/Directories.qml b/dot_files/quickshell/modules/common/Directories.qml new file mode 100644 index 0000000..95c0022 --- /dev/null +++ b/dot_files/quickshell/modules/common/Directories.qml @@ -0,0 +1,57 @@ +pragma Singleton + +import QtQuick +import Quickshell + +// XDG and application directory paths +QtObject { + id: root + + // XDG Base directories + readonly property string home: Quickshell.env("HOME") || "/home" + readonly property string configHome: Quickshell.env("XDG_CONFIG_HOME") || (home + "/.config") + readonly property string dataHome: Quickshell.env("XDG_DATA_HOME") || (home + "/.local/share") + readonly property string cacheHome: Quickshell.env("XDG_CACHE_HOME") || (home + "/.cache") + readonly property string stateHome: Quickshell.env("XDG_STATE_HOME") || (home + "/.local/state") + readonly property string runtimeDir: Quickshell.env("XDG_RUNTIME_DIR") || ("/run/user/" + Quickshell.env("UID")) + + // Application-specific directories + readonly property string hypercubeConfig: configHome + "/hypercube" + readonly property string hypercubeData: dataHome + "/hypercube" + readonly property string hypercubeCache: cacheHome + "/hypercube" + readonly property string hypercubeState: stateHome + "/hypercube" + + // Hyprland directories + readonly property string hyprlandConfig: configHome + "/hypr" + readonly property string hyprlandSocket: runtimeDir + "/hypr" + + // Common application directories + readonly property string applicationsUser: dataHome + "/applications" + readonly property string applicationsSystem: "/usr/share/applications" + readonly property string iconsUser: dataHome + "/icons" + readonly property string iconsSystem: "/usr/share/icons" + + // Wallpaper directories + readonly property string wallpapersUser: dataHome + "/wallpapers" + readonly property string wallpapersSystem: "/usr/share/wallpapers" + readonly property string picturesDir: Quickshell.env("XDG_PICTURES_DIR") || (home + "/Pictures") + + // Screenshots directory + readonly property string screenshotsDir: picturesDir + "/Screenshots" + + // Quickshell directories + readonly property string quickshellConfig: configHome + "/quickshell" + readonly property string quickshellData: dataHome + "/quickshell" + + // Helper to check if a path exists (would need Process to implement) + function exists(path: string): bool { + // This is a placeholder - actual implementation would use Process + return true + } + + // Helper to ensure directory exists + function ensureDir(path: string): bool { + // This is a placeholder - actual implementation would use Process + return true + } +} diff --git a/dot_files/quickshell/modules/common/Icons.qml b/dot_files/quickshell/modules/common/Icons.qml new file mode 100644 index 0000000..05ac24f --- /dev/null +++ b/dot_files/quickshell/modules/common/Icons.qml @@ -0,0 +1,216 @@ +pragma Singleton + +import QtQuick + +// Icon mappings using Nerd Font icons +QtObject { + id: root + + // Nerd Font icon codes + readonly property var icons: ({ + // System + settings: "\uf013", // + power: "\uf011", // + restart: "\uf01e", // + logout: "\uf2f5", // + lock: "\uf023", // + sleep: "\uf186", // + + // Audio + volumeHigh: "\uf028", // + volumeMedium: "\uf027", // + volumeLow: "\uf026", // + volumeMute: "\uf6a9", // + volumeOff: "\uf6a9", // (alias for mute) + mic: "\uf130", // + micOff: "\uf131", // + headphones: "\uf025", // + speaker: "\uf028", // + + // Brightness + brightnessHigh: "\uf185", // + brightnessMedium: "\uf111", // + brightnessLow: "\uf0eb", // + + // Network + wifi: "\uf1eb", // + wifiOff: "\uf1eb", // (with color change) + wifiWeak: "\uf1eb", // + wifiMedium: "\uf1eb", // + wifiStrong: "\uf1eb", // + ethernet: "\uf6ff", // + ethernetOff: "\uf6ff", // + vpn: "\uf084", // + airplane: "\uf072", // + + // Bluetooth + bluetooth: "\uf294", // + bluetoothOff: "\uf294", // + bluetoothConnected: "\uf294", // + bluetoothSearching: "\uf294", // + + // Battery + battery: "\uf240", // + battery90: "\uf241", // + battery80: "\uf241", // + battery60: "\uf242", // + battery40: "\uf243", // + battery20: "\uf244", // + battery10: "\uf244", // + batteryEmpty: "\uf244", // + batteryCharging: "\uf0e7", // + batteryAlert: "\uf071", // + + // Notifications + notification: "\uf0f3", // + notificationOff: "\uf1f6", // + notificationActive: "\uf0f3", // + notificationNone: "\uf0a2", // + doNotDisturb: "\uf05e", // + + // Privacy + camera: "\uf03d", // + cameraOff: "\uf03d", // + screenShare: "\uf108", // + screenShareOff: "\uf108", // + + // Time & Calendar + clock: "\uf017", // + calendar: "\uf073", // + today: "\uf073", // + event: "\uf073", // + alarm: "\uf0f3", // + timer: "\uf2f2", // + + // Weather + sunny: "\uf185", // + partlyCloudy: "\uf6c4", // + cloudy: "\uf0c2", // + rain: "\uf043", // + heavyRain: "\uf0e9", // + snow: "\uf2dc", // + fog: "\uf75f", // + wind: "\uf72e", // + night: "\uf186", // + nightCloudy: "\uf6c3", // + + // Navigation + menu: "\uf0c9", // + close: "\uf00d", // + back: "\uf060", // + forward: "\uf061", // + up: "\uf062", // + down: "\uf063", // + expand: "\uf078", // + collapse: "\uf077", // + search: "\uf002", // + filter: "\uf0b0", // + + // Actions + add: "\uf067", // + remove: "\uf068", // + delete: "\uf1f8", // + edit: "\uf044", // + copy: "\uf0c5", // + paste: "\uf0ea", // + refresh: "\uf021", // + check: "\uf00c", // + checkCircle: "\uf058", // + error: "\uf057", // + warning: "\uf071", // + info: "\uf05a", // + help: "\uf059", // + + // Apps & Categories + apps: "\uf009", // + grid: "\uf00a", // + list: "\uf03a", // + folder: "\uf07b", // + file: "\uf15b", // + image: "\uf03e", // + video: "\uf03d", // + music: "\uf001", // + download: "\uf019", // + upload: "\uf093", // + + // User & Account + person: "\uf007", // + personAdd: "\uf234", // + group: "\uf0c0", // + account: "\uf2bd", // + + // Media controls + play: "\uf04b", // + pause: "\uf04c", // + stop: "\uf04d", // + skipPrevious: "\uf048", // + skipNext: "\uf051", // + shuffle: "\uf074", // + repeat: "\uf01e", // + repeatOne: "\uf01e", // + + // Misc + star: "\uf005", // + starOutline: "\uf006", // + heart: "\uf004", // + heartOutline: "\uf08a", // + pin: "\uf08d", // + link: "\uf0c1", // + code: "\uf121", // + terminal: "\uf120", // + update: "\uf021", // + sync: "\uf021" // + }) + + // Get icon for battery level + function batteryIcon(level, charging) { + if (charging) return icons.batteryCharging + if (level >= 90) return icons.battery + if (level >= 80) return icons.battery90 + if (level >= 60) return icons.battery80 + if (level >= 40) return icons.battery60 + if (level >= 20) return icons.battery40 + if (level >= 10) return icons.battery20 + return icons.batteryEmpty + } + + // Get icon for volume level + function volumeIcon(level, muted) { + if (muted) return icons.volumeMute + if (level >= 66) return icons.volumeHigh + if (level >= 33) return icons.volumeMedium + return icons.volumeLow + } + + // Get icon for brightness level + function brightnessIcon(level) { + if (level >= 66) return icons.brightnessHigh + if (level >= 33) return icons.brightnessMedium + return icons.brightnessLow + } + + // Get icon for wifi strength + function wifiIcon(strength, connected) { + if (!connected) return icons.wifiOff + return icons.wifi + } + + // Get icon for weather condition + function weatherIcon(condition, isNight) { + if (!condition) return icons.cloudy + const lc = condition.toLowerCase() + if (lc.includes("clear") || lc.includes("sunny")) { + return isNight ? icons.night : icons.sunny + } + if (lc.includes("partly") || lc.includes("few clouds")) { + return isNight ? icons.nightCloudy : icons.partlyCloudy + } + if (lc.includes("cloud") || lc.includes("overcast")) return icons.cloudy + if (lc.includes("thunder") || lc.includes("storm")) return icons.heavyRain + if (lc.includes("rain") || lc.includes("drizzle")) return icons.rain + if (lc.includes("snow") || lc.includes("sleet")) return icons.snow + if (lc.includes("fog") || lc.includes("mist") || lc.includes("haze")) return icons.fog + if (lc.includes("wind")) return icons.wind + return icons.cloudy + } +} diff --git a/dot_files/quickshell/modules/common/qmldir b/dot_files/quickshell/modules/common/qmldir new file mode 100644 index 0000000..893aa1a --- /dev/null +++ b/dot_files/quickshell/modules/common/qmldir @@ -0,0 +1,7 @@ +module common + +singleton Appearance 1.0 Appearance.qml +singleton Config 1.0 Config.qml +singleton Directories 1.0 Directories.qml +singleton Icons 1.0 Icons.qml +ClickCatcher 1.0 ClickCatcher.qml diff --git a/dot_files/quickshell/modules/notifications/NotificationPopup.qml b/dot_files/quickshell/modules/notifications/NotificationPopup.qml new file mode 100644 index 0000000..2794545 --- /dev/null +++ b/dot_files/quickshell/modules/notifications/NotificationPopup.qml @@ -0,0 +1,358 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +import "../common" as Common +import "../../" as Root +import "../../services" as Services + +// Notification popup that appears in the corner +PanelWindow { + id: root + + required property var targetScreen + screen: targetScreen + + anchors { + top: true + right: true + } + + margins.top: Common.Appearance.sizes.barHeight + Common.Appearance.spacing.medium + margins.right: Common.Appearance.spacing.medium + + implicitWidth: Common.Appearance.sizes.notificationWidth + implicitHeight: notificationColumn.implicitHeight + color: "transparent" + + visible: notifications.length > 0 && !Root.GlobalStates.doNotDisturb + + // Notification list + property var notifications: [] + property int maxVisible: 5 + + // Background + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.large + color: "transparent" + } + + ColumnLayout { + id: notificationColumn + anchors.fill: parent + spacing: Common.Appearance.spacing.small + + Repeater { + model: notifications.slice(0, maxVisible) + + delegate: NotificationItem { + Layout.fillWidth: true + notification: modelData + onDismissed: removeNotification(modelData.id) + onActionInvoked: (actionId) => invokeAction(modelData, actionId) + } + } + + // "More notifications" indicator + Rectangle { + visible: notifications.length > maxVisible + Layout.fillWidth: true + Layout.preferredHeight: 32 + radius: Common.Appearance.rounding.medium + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + Common.Appearance.overlayOpacity + ) + + Text { + anchors.centerIn: parent + text: "+" + (notifications.length - maxVisible) + " more notifications" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.sidebarRightOpen = true + } + } + } + + // Notification item component + component NotificationItem: Rectangle { + id: notifItem + + property var notification + signal dismissed() + signal actionInvoked(string actionId) + + implicitHeight: contentLayout.implicitHeight + Common.Appearance.spacing.medium * 2 + radius: Common.Appearance.rounding.large + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + Common.Appearance.overlayOpacity + ) + + border.width: 1 + border.color: Common.Appearance.m3colors.outlineVariant + + // Auto-dismiss timer + Timer { + id: dismissTimer + interval: Common.Config.notificationTimeout + running: true + onTriggered: dismissed() + } + + // Pause timer on hover + MouseArea { + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: { + if (containsMouse) { + dismissTimer.stop() + } else { + dismissTimer.restart() + } + } + } + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.small + + // Header row + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + // App icon with datacube fallback + Item { + id: popupIconContainer + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + + property string iconName: notification.appIcon || "" + property string datacubeIcon: "" + property bool datacubeQueried: false + property bool iconLoaded: datacubePopupIcon.status === Image.Ready || primaryPopupIcon.status === Image.Ready + visible: iconLoaded + + Component.onCompleted: { + if (notification.appName && !datacubeQueried) { + datacubeQueried = true + popupIconLookup.query = notification.appName + popupIconLookup.running = true + } + } + + Process { + id: popupIconLookup + property string query: "" + command: ["bash", "-lc", "datacube-cli query '" + query.replace(/'/g, "'\\''") + "' --json -m 1"] + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + if (!data || data.trim() === "") return + try { + const item = JSON.parse(data) + if (item.icon) { + if (item.icon.startsWith("/")) { + popupIconContainer.datacubeIcon = "file://" + item.icon + } else { + popupIconContainer.datacubeIcon = "image://icon/" + item.icon + } + } + } catch (e) {} + } + } + } + + // Primary: Try datacube icon first + Image { + id: datacubePopupIcon + anchors.fill: parent + source: popupIconContainer.datacubeIcon + sourceSize: Qt.size(20, 20) + smooth: true + visible: status === Image.Ready + } + + // Fallback: Qt icon provider + Image { + id: primaryPopupIcon + anchors.fill: parent + source: popupIconContainer.iconName && datacubePopupIcon.status !== Image.Ready + ? "image://icon/" + popupIconContainer.iconName + : "" + sourceSize: Qt.size(20, 20) + smooth: true + visible: datacubePopupIcon.status !== Image.Ready && status === Image.Ready + } + } + + // App name + Text { + Layout.fillWidth: true + text: notification.appName || "Notification" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + elide: Text.ElideRight + } + + // Time + Text { + text: formatTime(notification.time) + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.smallest + color: Common.Appearance.m3colors.onSurfaceVariant + } + + // Close button + MouseArea { + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + cursorShape: Qt.PointingHandCursor + onClicked: dismissed() + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + // Content row + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.medium + + // Notification image + Image { + visible: notification.image && status === Image.Ready + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + source: notification.image || "" + sourceSize: Qt.size(48, 48) + fillMode: Image.PreserveAspectCrop + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + // Summary + Text { + Layout.fillWidth: true + text: notification.summary || "" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + elide: Text.ElideRight + wrapMode: Text.WordWrap + maximumLineCount: 2 + } + + // Body + Text { + Layout.fillWidth: true + visible: text !== "" + text: notification.body || "" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + elide: Text.ElideRight + wrapMode: Text.WordWrap + maximumLineCount: 3 + } + } + } + + // Actions row + RowLayout { + visible: notification.actions && notification.actions.length > 0 + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Repeater { + model: notification.actions || [] + + delegate: MouseArea { + required property var modelData + Layout.preferredHeight: 28 + Layout.preferredWidth: actionText.implicitWidth + Common.Appearance.spacing.medium * 2 + cursorShape: Qt.PointingHandCursor + onClicked: actionInvoked(modelData.identifier || "") + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse + ? Common.Appearance.m3colors.primaryContainer + : Common.Appearance.m3colors.surfaceVariant + } + + Text { + id: actionText + anchors.centerIn: parent + text: modelData.text || "Action" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.primary + } + } + } + } + } + } + + function formatTime(timestamp: var): string { + if (!timestamp) return "" + const now = new Date() + const then = new Date(timestamp) + const diff = Math.floor((now - then) / 1000) + + if (diff < 60) return "now" + if (diff < 3600) return Math.floor(diff / 60) + "m" + if (diff < 86400) return Math.floor(diff / 3600) + "h" + return then.toLocaleDateString() + } + + function addNotification(notification: var) { + // Add to front of list + notifications = [notification, ...notifications] + Root.GlobalStates.unreadNotificationCount++ + } + + function removeNotification(id: var) { + notifications = notifications.filter(n => n.id !== id) + } + + function invokeAction(notification: var, actionId: string) { + // Invoke action through the notification service + Services.Notifications.invokeAction(notification.id, actionId) + removeNotification(notification.id) + } + + function clearAll() { + notifications = [] + Root.GlobalStates.unreadNotificationCount = 0 + } +} diff --git a/dot_files/quickshell/modules/notifications/qmldir b/dot_files/quickshell/modules/notifications/qmldir new file mode 100644 index 0000000..8991a12 --- /dev/null +++ b/dot_files/quickshell/modules/notifications/qmldir @@ -0,0 +1,3 @@ +module notifications + +NotificationPopup 1.0 NotificationPopup.qml diff --git a/dot_files/quickshell/modules/osd/Osd.qml b/dot_files/quickshell/modules/osd/Osd.qml new file mode 100644 index 0000000..5436e0f --- /dev/null +++ b/dot_files/quickshell/modules/osd/Osd.qml @@ -0,0 +1,163 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +import "../common" as Common +import "../../" as Root + +PanelWindow { + id: root + + required property var targetScreen + screen: targetScreen + + // Center at the bottom of the screen + anchors.bottom: true + + // Use margins to center horizontally + margins.bottom: 100 + + implicitWidth: Common.Appearance.sizes.osdWidth + implicitHeight: Common.Appearance.sizes.osdHeight + Common.Appearance.spacing.large + color: "transparent" + + visible: Root.GlobalStates.osdVisible + + // OSD background + Rectangle { + id: osdBackground + anchors.fill: parent + + // Animation for showing/hiding + opacity: Root.GlobalStates.osdVisible ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Common.Appearance.animation.standard + easing.type: Easing.OutCubic + } + } + radius: Common.Appearance.rounding.large + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + Common.Appearance.overlayOpacity + ) + + // Border + border.width: 1 + border.color: Common.Appearance.m3colors.outlineVariant + } + + // OSD content + RowLayout { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.medium + + // Icon + Text { + id: osdIcon + text: getIcon() + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconXLarge + color: getIconColor() + + function getIcon() { + switch (Root.GlobalStates.osdType) { + case "volume": + return Root.GlobalStates.osdMuted + ? Common.Icons.icons.volumeMute + : Common.Icons.volumeIcon(Root.GlobalStates.osdValue * 100, false) + case "brightness": + return Common.Icons.brightnessIcon(Root.GlobalStates.osdValue * 100) + case "mic": + return Root.GlobalStates.osdMuted + ? Common.Icons.icons.micOff + : Common.Icons.icons.mic + default: + return Common.Icons.icons.volumeHigh + } + } + + function getIconColor() { + if (Root.GlobalStates.osdMuted) { + return Common.Appearance.m3colors.error + } + return Common.Appearance.m3colors.primary + } + } + + // Progress bar and value + ColumnLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.tiny + + // Progress bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 6 + radius: 3 + color: Common.Appearance.m3colors.surfaceVariant + + Rectangle { + width: parent.width * Math.min(Root.GlobalStates.osdValue, 1.0) + height: parent.height + radius: parent.radius + color: Root.GlobalStates.osdMuted + ? Common.Appearance.m3colors.error + : Common.Appearance.m3colors.primary + + Behavior on width { + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + } + + // Overshoot indicator (for volume > 100%) + Rectangle { + visible: Root.GlobalStates.osdValue > 1.0 && Root.GlobalStates.osdType === "volume" + x: parent.width + width: Math.min((Root.GlobalStates.osdValue - 1.0) * parent.width, parent.width * 0.5) + height: parent.height + radius: parent.radius + color: Common.Appearance.m3colors.error + opacity: 0.7 + + Behavior on width { + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + } + } + + // Value text (if enabled) + Text { + visible: Common.Config.osdShowValue + Layout.alignment: Qt.AlignRight + text: getValueText() + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + + function getValueText() { + if (Root.GlobalStates.osdMuted) { + return Root.GlobalStates.osdType === "mic" ? "Muted" : "Muted" + } + return Math.round(Root.GlobalStates.osdValue * 100) + "%" + } + } + } + } + + // Mouse area to dismiss on click + MouseArea { + anchors.fill: parent + onClicked: Root.GlobalStates.osdVisible = false + } +} diff --git a/dot_files/quickshell/modules/osd/qmldir b/dot_files/quickshell/modules/osd/qmldir new file mode 100644 index 0000000..edf98eb --- /dev/null +++ b/dot_files/quickshell/modules/osd/qmldir @@ -0,0 +1,3 @@ +module osd + +Osd 1.0 Osd.qml diff --git a/dot_files/quickshell/modules/sidebars/AudioView.qml b/dot_files/quickshell/modules/sidebars/AudioView.qml new file mode 100644 index 0000000..03f4a9e --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/AudioView.qml @@ -0,0 +1,557 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell + +import "../common" as Common +import "../../services" as Services +import "../../" as Root + +// Audio settings view for the right sidebar +ColumnLayout { + id: root + spacing: Common.Appearance.spacing.large + + // Refresh devices when view opens + Component.onCompleted: Services.Audio.refreshDevices() + + // Header with close button + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Text { + Layout.fillWidth: true + text: "Audio" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.headline + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + MouseArea { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: Root.GlobalStates.sidebarRightOpen = false + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surfaceVariant : "transparent" + } + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurface + } + } + } + + // Output (Speaker) section + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: outputContent.implicitHeight + Common.Appearance.spacing.medium * 2 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + ColumnLayout { + id: outputContent + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.medium + + // Header row with mute toggle + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.medium + + Text { + text: Services.Audio.muted + ? Common.Icons.icons.volumeOff + : Common.Icons.volumeIcon(Services.Audio.volume * 100, false) + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconLarge + color: Services.Audio.muted + ? Common.Appearance.m3colors.onSurfaceVariant + : Common.Appearance.m3colors.primary + } + + ColumnLayout { + spacing: 2 + + Text { + text: "Speaker" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + Text { + text: Services.Audio.muted + ? "Muted" + : Math.round(Services.Audio.volume * 100) + "%" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + Item { Layout.fillWidth: true } + + // Mute toggle switch + MouseArea { + Layout.preferredWidth: 52 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignRight + cursorShape: Qt.PointingHandCursor + onClicked: Services.Audio.toggleMute() + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: !Services.Audio.muted + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.surfaceVariant + border.width: !Services.Audio.muted ? 0 : 2 + border.color: Common.Appearance.m3colors.outline + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Rectangle { + width: !Services.Audio.muted ? 24 : 16 + height: !Services.Audio.muted ? 24 : 16 + radius: height / 2 + anchors.verticalCenter: parent.verticalCenter + x: !Services.Audio.muted ? parent.width - width - 4 : 4 + color: !Services.Audio.muted + ? Common.Appearance.m3colors.onPrimary + : Common.Appearance.m3colors.outline + + Behavior on x { + NumberAnimation { duration: 150; easing.type: Easing.InOutQuad } + } + Behavior on width { + NumberAnimation { duration: 150 } + } + Behavior on height { + NumberAnimation { duration: 150 } + } + Behavior on color { + ColorAnimation { duration: 150 } + } + } + } + } + } + + // Volume slider + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Text { + text: Common.Icons.icons.volumeLow + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + + // Custom slider using MouseArea + Item { + Layout.fillWidth: true + Layout.preferredHeight: 32 + + Rectangle { + id: volumeTrack + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: 6 + radius: 3 + color: Common.Appearance.m3colors.outline + + Rectangle { + width: Services.Audio.volume * parent.width + height: parent.height + radius: 3 + color: Common.Appearance.m3colors.primary + } + } + + Rectangle { + id: volumeHandle + width: 20 + height: 20 + radius: 10 + x: Services.Audio.volume * (parent.width - width) + anchors.verticalCenter: parent.verticalCenter + color: Common.Appearance.m3colors.primaryContainer + border.color: Common.Appearance.m3colors.primary + border.width: 2 + } + + MouseArea { + anchors.fill: parent + onPressed: updateVolume(mouse) + onPositionChanged: if (pressed) updateVolume(mouse) + + function updateVolume(mouse) { + let newValue = Math.max(0, Math.min(1, mouse.x / width)) + Services.Audio.setVolume(newValue) + } + } + } + + Text { + text: Common.Icons.icons.volumeHigh + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + // Output device selector + DeviceSelector { + Layout.fillWidth: true + label: "Output Device" + devices: Services.Audio.sinks + onDeviceSelected: function(name) { + Services.Audio.setDefaultSink(name) + } + } + } + } + + // Input (Microphone) section + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: inputContent.implicitHeight + Common.Appearance.spacing.medium * 2 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + ColumnLayout { + id: inputContent + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.medium + + // Header row with mute toggle + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.medium + + Text { + text: Services.Audio.micMuted + ? Common.Icons.icons.micOff + : Common.Icons.icons.mic + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconLarge + color: Services.Privacy.micInUse + ? Common.Appearance.m3colors.error + : (Services.Audio.micMuted + ? Common.Appearance.m3colors.onSurfaceVariant + : Common.Appearance.m3colors.primary) + } + + ColumnLayout { + spacing: 2 + + Text { + text: "Microphone" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + Text { + text: { + if (Services.Audio.micMuted && Services.Privacy.micInUse) { + return "Muted (in use)" + } else if (Services.Audio.micMuted) { + return "Muted" + } else if (Services.Privacy.micInUse) { + return "In use - " + Math.round(Services.Audio.micVolume * 100) + "%" + } else { + return Math.round(Services.Audio.micVolume * 100) + "%" + } + } + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Services.Privacy.micInUse + ? Common.Appearance.m3colors.error + : Common.Appearance.m3colors.onSurfaceVariant + } + } + + Item { Layout.fillWidth: true } + + // Mute toggle switch + MouseArea { + Layout.preferredWidth: 52 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignRight + cursorShape: Qt.PointingHandCursor + onClicked: Services.Audio.toggleMicMute() + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: !Services.Audio.micMuted + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.surfaceVariant + border.width: !Services.Audio.micMuted ? 0 : 2 + border.color: Common.Appearance.m3colors.outline + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Rectangle { + width: !Services.Audio.micMuted ? 24 : 16 + height: !Services.Audio.micMuted ? 24 : 16 + radius: height / 2 + anchors.verticalCenter: parent.verticalCenter + x: !Services.Audio.micMuted ? parent.width - width - 4 : 4 + color: !Services.Audio.micMuted + ? Common.Appearance.m3colors.onPrimary + : Common.Appearance.m3colors.outline + + Behavior on x { + NumberAnimation { duration: 150; easing.type: Easing.InOutQuad } + } + Behavior on width { + NumberAnimation { duration: 150 } + } + Behavior on height { + NumberAnimation { duration: 150 } + } + Behavior on color { + ColorAnimation { duration: 150 } + } + } + } + } + } + + // Mic volume slider + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Text { + text: Common.Icons.icons.micOff + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + + // Custom slider using MouseArea + Item { + Layout.fillWidth: true + Layout.preferredHeight: 32 + + property color accentColor: Services.Privacy.micInUse + ? Common.Appearance.m3colors.error + : Common.Appearance.m3colors.primary + + Rectangle { + id: micTrack + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: 6 + radius: 3 + color: Common.Appearance.m3colors.outline + + Rectangle { + width: Services.Audio.micVolume * parent.width + height: parent.height + radius: 3 + color: parent.parent.accentColor + } + } + + Rectangle { + id: micHandle + width: 20 + height: 20 + radius: 10 + x: Services.Audio.micVolume * (parent.width - width) + anchors.verticalCenter: parent.verticalCenter + color: Common.Appearance.m3colors.primaryContainer + border.color: parent.accentColor + border.width: 2 + } + + MouseArea { + anchors.fill: parent + onPressed: updateMicVolume(mouse) + onPositionChanged: if (pressed) updateMicVolume(mouse) + + function updateMicVolume(mouse) { + let newValue = Math.max(0, Math.min(1, mouse.x / width)) + Services.Audio.setMicVolume(newValue) + } + } + } + + Text { + text: Common.Icons.icons.mic + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + // Input device selector + DeviceSelector { + Layout.fillWidth: true + label: "Input Device" + devices: Services.Audio.sources + onDeviceSelected: function(name) { + Services.Audio.setDefaultSource(name) + } + } + } + } + + // Spacer + Item { Layout.fillHeight: true } + + // Device selector dropdown component + component DeviceSelector: ColumnLayout { + id: selector + property string label: "" + property var devices: [] + property bool expanded: false + + signal deviceSelected(string name) + + spacing: Common.Appearance.spacing.small + + // Current selection / dropdown trigger + MouseArea { + Layout.fillWidth: true + Layout.preferredHeight: 40 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: selector.expanded = !selector.expanded + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.medium + color: parent.containsMouse ? Common.Appearance.surfaceLayer(2) : Common.Appearance.surfaceLayer(1) + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.small + anchors.rightMargin: Common.Appearance.spacing.small + spacing: Common.Appearance.spacing.small + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + Text { + text: selector.label + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + + Text { + Layout.fillWidth: true + text: { + for (let i = 0; i < selector.devices.length; i++) { + if (selector.devices[i].isDefault) { + return selector.devices[i].description + } + } + return "No device" + } + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + elide: Text.ElideRight + } + } + + Text { + text: selector.expanded ? Common.Icons.icons.expand : Common.Icons.icons.collapse + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + // Dropdown list + ColumnLayout { + visible: selector.expanded + Layout.fillWidth: true + spacing: 2 + + Repeater { + model: selector.devices + + MouseArea { + Layout.fillWidth: true + Layout.preferredHeight: 36 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: { + selector.deviceSelected(modelData.name) + selector.expanded = false + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: modelData.isDefault + ? Common.Appearance.m3colors.primaryContainer + : (parent.containsMouse ? Common.Appearance.surfaceLayer(2) : "transparent") + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.small + anchors.rightMargin: Common.Appearance.spacing.small + spacing: Common.Appearance.spacing.small + + Text { + Layout.fillWidth: true + text: modelData.description + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: modelData.isDefault + ? Common.Appearance.m3colors.onPrimaryContainer + : Common.Appearance.m3colors.onSurface + elide: Text.ElideRight + } + + Text { + visible: modelData.isDefault + text: Common.Icons.icons.check + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onPrimaryContainer + } + } + } + } + } + } +} diff --git a/dot_files/quickshell/modules/sidebars/BluetoothView.qml b/dot_files/quickshell/modules/sidebars/BluetoothView.qml new file mode 100644 index 0000000..840937f --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/BluetoothView.qml @@ -0,0 +1,529 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell + +import "../common" as Common +import "../../services" as Services +import "../../" as Root + +// Bluetooth settings view for the right sidebar +ColumnLayout { + id: root + spacing: Common.Appearance.spacing.large + + // Refresh device list when view opens + Component.onCompleted: Services.BluetoothStatus.refresh() + + // Clear available devices when view closes + Component.onDestruction: { + Services.BluetoothStatus.stopDiscovery() + Services.BluetoothStatus.clearAvailableDevices() + } + + // Header with close button + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Text { + Layout.fillWidth: true + text: "Bluetooth" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.headline + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + MouseArea { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: Root.GlobalStates.sidebarRightOpen = false + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surfaceVariant : "transparent" + } + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurface + } + } + } + + // Power toggle + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 56 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.medium + anchors.rightMargin: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.medium + + Text { + text: Common.Icons.icons.bluetooth + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconLarge + color: Services.BluetoothStatus.powered + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.onSurfaceVariant + } + + ColumnLayout { + spacing: 2 + + Text { + text: "Bluetooth" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + Text { + text: Services.BluetoothStatus.powered + ? (Services.BluetoothStatus.connected + ? "Connected to " + Services.BluetoothStatus.connectedDeviceName + : "On") + : "Off" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + Item { Layout.fillWidth: true } + + // Modern rounded switch + MouseArea { + Layout.preferredWidth: 52 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignRight + cursorShape: Qt.PointingHandCursor + onClicked: Services.BluetoothStatus.setPower(!Services.BluetoothStatus.powered) + + Rectangle { + id: btSwitchTrack + anchors.fill: parent + radius: height / 2 + color: Services.BluetoothStatus.powered + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.surfaceVariant + border.width: Services.BluetoothStatus.powered ? 0 : 2 + border.color: Common.Appearance.m3colors.outline + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Rectangle { + id: btSwitchThumb + width: Services.BluetoothStatus.powered ? 24 : 16 + height: Services.BluetoothStatus.powered ? 24 : 16 + radius: height / 2 + anchors.verticalCenter: parent.verticalCenter + x: Services.BluetoothStatus.powered ? parent.width - width - 4 : 4 + color: Services.BluetoothStatus.powered + ? Common.Appearance.m3colors.onPrimary + : Common.Appearance.m3colors.outline + + Behavior on x { + NumberAnimation { duration: 150; easing.type: Easing.InOutQuad } + } + Behavior on width { + NumberAnimation { duration: 150 } + } + Behavior on height { + NumberAnimation { duration: 150 } + } + Behavior on color { + ColorAnimation { duration: 150 } + } + } + } + } + } + } + + // ===== SECTION 1: Known/Paired Devices ===== + ColumnLayout { + visible: Services.BluetoothStatus.powered + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + // Section header + Text { + text: "My Devices" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurfaceVariant + } + + // Paired devices list + Rectangle { + visible: Services.BluetoothStatus.devices.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: pairedDevicesColumn.implicitHeight + Common.Appearance.spacing.medium * 2 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + ColumnLayout { + id: pairedDevicesColumn + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.small + + Repeater { + model: Services.BluetoothStatus.devices + + delegate: PairedDeviceItem { + Layout.fillWidth: true + deviceName: modelData.name + deviceMac: modelData.mac + isConnected: modelData.status === "connected" + onConnectClicked: Services.BluetoothStatus.connectDevice(modelData.mac) + onDisconnectClicked: Services.BluetoothStatus.disconnectDevice(modelData.mac) + onRemoveClicked: Services.BluetoothStatus.forgetDevice(modelData.mac) + } + } + } + } + + // No paired devices message + Rectangle { + visible: Services.BluetoothStatus.devices.length === 0 + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + Text { + anchors.centerIn: parent + text: "No paired devices" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + // ===== SECTION 2: Available/Discoverable Devices ===== + ColumnLayout { + visible: Services.BluetoothStatus.powered + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + // Section header with scan button + RowLayout { + Layout.fillWidth: true + + Text { + text: "Available Devices" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurfaceVariant + } + + Item { Layout.fillWidth: true } + + MouseArea { + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (Services.BluetoothStatus.discovering) { + Services.BluetoothStatus.stopDiscovery() + } else { + Services.BluetoothStatus.startDiscovery() + } + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surfaceVariant : "transparent" + } + + Text { + id: refreshIcon + anchors.centerIn: parent + text: Common.Icons.icons.refresh + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Services.BluetoothStatus.discovering + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.onSurfaceVariant + + RotationAnimation on rotation { + running: Services.BluetoothStatus.discovering + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + } + } + } + } + + // Scanning state + Rectangle { + visible: Services.BluetoothStatus.discovering && Services.BluetoothStatus.availableDevices.length === 0 + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + Text { + anchors.centerIn: parent + text: "Scanning for devices..." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + // Available devices list (only shown when discovering or has results) + Rectangle { + visible: Services.BluetoothStatus.availableDevices.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: availableDevicesColumn.implicitHeight + Common.Appearance.spacing.medium * 2 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + ColumnLayout { + id: availableDevicesColumn + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.small + + Repeater { + model: Services.BluetoothStatus.availableDevices + + delegate: AvailableDeviceItem { + Layout.fillWidth: true + deviceName: modelData.name + deviceMac: modelData.mac + onPairClicked: Services.BluetoothStatus.pairDevice(modelData.mac) + } + } + } + } + + // Idle state - prompt to scan + Rectangle { + visible: !Services.BluetoothStatus.discovering && Services.BluetoothStatus.availableDevices.length === 0 + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + Text { + anchors.centerIn: parent + text: "Tap refresh to scan" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + // Spacer + Item { Layout.fillHeight: true } + + // ===== Paired Device Item Component ===== + component PairedDeviceItem: MouseArea { + id: pairedItem + property string deviceName: "" + property string deviceMac: "" + property bool isConnected: false + + signal connectClicked() + signal disconnectClicked() + signal removeClicked() + + implicitHeight: 48 + hoverEnabled: true + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.medium + color: pairedItem.containsMouse ? Common.Appearance.surfaceLayer(2) : "transparent" + } + + RowLayout { + anchors.fill: parent + spacing: Common.Appearance.spacing.medium + + // Device icon + Text { + text: pairedItem.isConnected + ? Common.Icons.icons.bluetoothConnected + : Common.Icons.icons.bluetooth + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: pairedItem.isConnected + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.onSurfaceVariant + } + + // Device info + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: pairedItem.deviceName || pairedItem.deviceMac + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: pairedItem.isConnected ? "Connected" : "Not connected" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: pairedItem.isConnected + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.onSurfaceVariant + } + } + + // Action buttons (visible on hover) + RowLayout { + visible: pairedItem.containsMouse + spacing: Common.Appearance.spacing.tiny + + // Connect/Disconnect button + MouseArea { + Layout.preferredWidth: 28 + Layout.preferredHeight: 28 + cursorShape: Qt.PointingHandCursor + onClicked: { + if (pairedItem.isConnected) { + pairedItem.disconnectClicked() + } else { + pairedItem.connectClicked() + } + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse + ? Common.Appearance.m3colors.primaryContainer + : "transparent" + } + + Text { + anchors.centerIn: parent + text: pairedItem.isConnected + ? Common.Icons.icons.close + : Common.Icons.icons.bluetooth + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurface + } + } + + // Remove/Forget button + MouseArea { + Layout.preferredWidth: 28 + Layout.preferredHeight: 28 + cursorShape: Qt.PointingHandCursor + onClicked: pairedItem.removeClicked() + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse + ? Common.Appearance.m3colors.errorContainer + : "transparent" + } + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.delete + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: parent.containsMouse + ? Common.Appearance.m3colors.onErrorContainer + : Common.Appearance.m3colors.onSurfaceVariant + } + } + } + } + } + + // ===== Available Device Item Component ===== + component AvailableDeviceItem: MouseArea { + id: availableItem + property string deviceName: "" + property string deviceMac: "" + + signal pairClicked() + + implicitHeight: 48 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: pairClicked() + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.medium + color: availableItem.containsMouse ? Common.Appearance.surfaceLayer(2) : "transparent" + } + + RowLayout { + anchors.fill: parent + spacing: Common.Appearance.spacing.medium + + // Device icon + Text { + text: Common.Icons.icons.bluetooth + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurfaceVariant + } + + // Device info + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: availableItem.deviceName || availableItem.deviceMac + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: "Tap to pair" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + } +} diff --git a/dot_files/quickshell/modules/sidebars/CalendarView.qml b/dot_files/quickshell/modules/sidebars/CalendarView.qml new file mode 100644 index 0000000..1e72054 --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/CalendarView.qml @@ -0,0 +1,235 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell + +import "../common" as Common +import "../../services" as Services +import "../../" as Root + +// Calendar view for the right sidebar +Flickable { + id: root + contentHeight: contentColumn.height + clip: true + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + ColumnLayout { + id: contentColumn + width: parent.width + spacing: Common.Appearance.spacing.large + + // Header with close button + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Text { + Layout.fillWidth: true + text: "Calendar" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.headline + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + MouseArea { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: Root.GlobalStates.sidebarRightOpen = false + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surfaceVariant : "transparent" + } + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurface + } + } + } + + // Calendar card + Rectangle { + Layout.fillWidth: true + implicitHeight: calendarContent.implicitHeight + Common.Appearance.spacing.medium * 2 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + ColumnLayout { + id: calendarContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.small + + property int displayMonth: new Date().getMonth() + property int displayYear: new Date().getFullYear() + + // Month navigation + RowLayout { + Layout.fillWidth: true + + MouseArea { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (calendarContent.displayMonth === 0) { + calendarContent.displayMonth = 11 + calendarContent.displayYear-- + } else { + calendarContent.displayMonth-- + } + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surface : "transparent" + } + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.back + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurface + } + } + + Text { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: { + var months = Services.DateTime.monthNames + if (months && months.length > calendarContent.displayMonth) { + return months[calendarContent.displayMonth] + " " + calendarContent.displayYear + } + return calendarContent.displayYear.toString() + } + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + MouseArea { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (calendarContent.displayMonth === 11) { + calendarContent.displayMonth = 0 + calendarContent.displayYear++ + } else { + calendarContent.displayMonth++ + } + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surface : "transparent" + } + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.forward + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurface + } + } + } + + // Day headers + RowLayout { + Layout.fillWidth: true + spacing: 0 + + Repeater { + model: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] + + Text { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: modelData + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + // Calendar grid + Grid { + id: calendarGrid + Layout.fillWidth: true + columns: 7 + spacing: 2 + + Repeater { + model: 42 + + Rectangle { + width: Math.max((calendarGrid.width - 12) / 7, 20) + height: width + radius: width / 2 + + property int dayNumber: { + var firstDay = new Date(calendarContent.displayYear, calendarContent.displayMonth, 1).getDay() + var daysInMonth = new Date(calendarContent.displayYear, calendarContent.displayMonth + 1, 0).getDate() + var dayIndex = index - firstDay + 1 + + if (dayIndex < 1 || dayIndex > daysInMonth) { + return 0 + } + return dayIndex + } + + property bool isToday: { + var now = new Date() + return dayNumber === now.getDate() && + calendarContent.displayMonth === now.getMonth() && + calendarContent.displayYear === now.getFullYear() + } + + color: isToday + ? Common.Appearance.m3colors.primary + : "transparent" + + Text { + anchors.centerIn: parent + text: dayNumber > 0 ? dayNumber : "" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: isToday + ? Common.Appearance.m3colors.onPrimary + : Common.Appearance.m3colors.onSurface + } + } + } + } + } + } + + // Spacer + Item { Layout.fillHeight: true } + } +} diff --git a/dot_files/quickshell/modules/sidebars/DefaultView.qml b/dot_files/quickshell/modules/sidebars/DefaultView.qml new file mode 100644 index 0000000..f4e84d5 --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/DefaultView.qml @@ -0,0 +1,10 @@ +import QtQuick +import QtQuick.Layouts + +import "../common" as Common +import "../../" as Root + +// Default/empty sidebar view +Item { + // Empty for now - views open directly from status bar icons +} diff --git a/dot_files/quickshell/modules/sidebars/NotificationsView.qml b/dot_files/quickshell/modules/sidebars/NotificationsView.qml new file mode 100644 index 0000000..8e16d93 --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/NotificationsView.qml @@ -0,0 +1,459 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Io + +import "../common" as Common +import "../../services" as Services +import "../../" as Root + +// Notifications view for the right sidebar +ColumnLayout { + id: root + spacing: Common.Appearance.spacing.large + + // Header with clear all and close button + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Text { + Layout.fillWidth: true + text: "Notifications" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.headline + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + MouseArea { + visible: Services.Notifications.notifications.length > 0 + Layout.preferredWidth: clearText.implicitWidth + Common.Appearance.spacing.medium * 2 + Layout.preferredHeight: 32 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + Services.Notifications.clearAll() + Root.GlobalStates.unreadNotificationCount = 0 + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surfaceVariant : "transparent" + } + + Text { + id: clearText + anchors.centerIn: parent + text: "Clear all" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.primary + } + } + + MouseArea { + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: Root.GlobalStates.sidebarRightOpen = false + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: parent.containsMouse ? Common.Appearance.m3colors.surfaceVariant : "transparent" + } + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurface + } + } + } + + // Do Not Disturb toggle card + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: dndContent.implicitHeight + Common.Appearance.spacing.medium * 2 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + RowLayout { + id: dndContent + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.medium + + Text { + text: Root.GlobalStates.doNotDisturb + ? Common.Icons.icons.doNotDisturb + : Common.Icons.icons.notification + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconLarge + color: Root.GlobalStates.doNotDisturb + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.onSurfaceVariant + } + + ColumnLayout { + spacing: 2 + + Text { + text: "Do Not Disturb" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + + Text { + text: Root.GlobalStates.doNotDisturb ? "On" : "Off" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + Item { Layout.fillWidth: true } + + // Modern rounded switch + MouseArea { + Layout.preferredWidth: 52 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignRight + cursorShape: Qt.PointingHandCursor + onClicked: Root.GlobalStates.doNotDisturb = !Root.GlobalStates.doNotDisturb + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: Root.GlobalStates.doNotDisturb + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.surfaceVariant + border.width: Root.GlobalStates.doNotDisturb ? 0 : 2 + border.color: Common.Appearance.m3colors.outline + + Behavior on color { + ColorAnimation { duration: 150 } + } + + Rectangle { + width: Root.GlobalStates.doNotDisturb ? 24 : 16 + height: Root.GlobalStates.doNotDisturb ? 24 : 16 + radius: height / 2 + anchors.verticalCenter: parent.verticalCenter + x: Root.GlobalStates.doNotDisturb ? parent.width - width - 4 : 4 + color: Root.GlobalStates.doNotDisturb + ? Common.Appearance.m3colors.onPrimary + : Common.Appearance.m3colors.outline + + Behavior on x { + NumberAnimation { duration: 150; easing.type: Easing.InOutQuad } + } + Behavior on width { + NumberAnimation { duration: 150 } + } + Behavior on height { + NumberAnimation { duration: 150 } + } + Behavior on color { + ColorAnimation { duration: 150 } + } + } + } + } + } + } + + // Notifications list + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + contentHeight: notificationsColumn.height + clip: true + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + ColumnLayout { + id: notificationsColumn + width: parent.width + spacing: Common.Appearance.spacing.small + + // Empty state + Rectangle { + visible: Services.Notifications.notifications.length === 0 + Layout.fillWidth: true + Layout.preferredHeight: 120 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + ColumnLayout { + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Text { + Layout.alignment: Qt.AlignHCenter + text: Common.Icons.icons.notification + font.family: Common.Appearance.fonts.icon + font.pixelSize: 32 + color: Common.Appearance.m3colors.onSurfaceVariant + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: "No notifications" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + // Notification list + Repeater { + model: Services.Notifications.notifications + + delegate: NotificationItem { + Layout.fillWidth: true + notification: modelData + onDismissed: { + Services.Notifications.removeNotification(modelData.id) + if (Root.GlobalStates.unreadNotificationCount > 0) { + Root.GlobalStates.unreadNotificationCount-- + } + } + onActionClicked: function(actionId) { + Services.Notifications.invokeAction(modelData.id, actionId) + if (Root.GlobalStates.unreadNotificationCount > 0) { + Root.GlobalStates.unreadNotificationCount-- + } + } + } + } + } + } + + // Notification item component + component NotificationItem: MouseArea { + id: notifItem + property var notification: ({}) + signal dismissed() + signal actionClicked(string actionId) + + Layout.fillWidth: true + Layout.preferredHeight: notifContent.implicitHeight + Common.Appearance.spacing.medium * 2 + hoverEnabled: true + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + RowLayout { + id: notifContent + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.small + + // App icon with datacube fallback + Item { + id: notifIconContainer + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + Layout.alignment: Qt.AlignTop + + property string iconName: notification.appIcon || "" + property string datacubeIcon: "" + property bool datacubeQueried: false + + Component.onCompleted: { + if (notification.appName && !datacubeQueried) { + datacubeQueried = true + iconLookup.query = notification.appName + iconLookup.running = true + } + } + + Process { + id: iconLookup + property string query: "" + command: ["bash", "-lc", "datacube-cli query '" + query.replace(/'/g, "'\\''") + "' --json -m 1"] + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + if (!data || data.trim() === "") return + try { + const item = JSON.parse(data) + if (item.icon) { + if (item.icon.startsWith("/")) { + notifIconContainer.datacubeIcon = "file://" + item.icon + } else { + notifIconContainer.datacubeIcon = "image://icon/" + item.icon + } + } + } catch (e) { + console.log("Icon lookup parse error:", e) + } + } + } + } + + // Primary: Try datacube icon first + Image { + id: datacubeNotifIcon + anchors.fill: parent + source: notifIconContainer.datacubeIcon + sourceSize: Qt.size(32, 32) + smooth: true + visible: status === Image.Ready + } + + // Fallback 1: Qt icon provider + Image { + id: primaryNotifIcon + anchors.fill: parent + source: notifIconContainer.iconName && datacubeNotifIcon.status !== Image.Ready + ? "image://icon/" + notifIconContainer.iconName + : "" + sourceSize: Qt.size(32, 32) + smooth: true + visible: datacubeNotifIcon.status !== Image.Ready && status === Image.Ready + } + + // Fallback 2: Letter icon + Rectangle { + anchors.fill: parent + visible: datacubeNotifIcon.status !== Image.Ready && primaryNotifIcon.status !== Image.Ready + radius: Common.Appearance.rounding.small + color: Common.Appearance.m3colors.primaryContainer + + Text { + anchors.centerIn: parent + text: notification.appName ? notification.appName.charAt(0).toUpperCase() : "?" + font.pixelSize: 14 + font.bold: true + color: Common.Appearance.m3colors.onPrimaryContainer + } + } + } + + // Content + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + // Header row + RowLayout { + Layout.fillWidth: true + + Text { + Layout.fillWidth: true + text: notification.summary || notification.appName || "Notification" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + elide: Text.ElideRight + } + + Text { + text: notifItem.formatTime(notification.time) + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + // Body + Text { + visible: notification.body && notification.body !== "" + Layout.fillWidth: true + text: notification.body + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + wrapMode: Text.WordWrap + maximumLineCount: 3 + elide: Text.ElideRight + } + + // Actions + RowLayout { + visible: notification.actions && notification.actions.length > 0 + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Repeater { + model: notification.actions || [] + + MouseArea { + required property var modelData + Layout.preferredHeight: 28 + Layout.preferredWidth: actionLabel.implicitWidth + Common.Appearance.spacing.medium * 2 + cursorShape: Qt.PointingHandCursor + onClicked: notifItem.actionClicked(modelData.identifier || "") + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.small + color: Common.Appearance.m3colors.primaryContainer + + Text { + id: actionLabel + anchors.centerIn: parent + text: modelData.text || "Action" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onPrimaryContainer + } + } + } + } + } + } + + // Dismiss button + MouseArea { + visible: notifItem.containsMouse + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignTop + cursorShape: Qt.PointingHandCursor + onClicked: notifItem.dismissed() + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + } + + function formatTime(date) { + if (!date) return "" + const now = new Date() + const diff = now - date + const mins = Math.floor(diff / 60000) + const hours = Math.floor(diff / 3600000) + + if (mins < 1) return "now" + if (mins < 60) return mins + "m" + if (hours < 24) return hours + "h" + return date.toLocaleDateString() + } + } +} diff --git a/dot_files/quickshell/modules/sidebars/SidebarLeft.qml b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml new file mode 100644 index 0000000..2ffe250 --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/SidebarLeft.qml @@ -0,0 +1,452 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +import "../common" as Common +import "../../" as Root + +PanelWindow { + id: root + + required property var targetScreen + screen: targetScreen + + anchors { + top: true + bottom: true + left: true + } + + margins.top: Common.Appearance.sizes.barHeight + + implicitWidth: Common.Appearance.sizes.sidebarWidth + color: "transparent" + + visible: Root.GlobalStates.sidebarLeftOpen + + // Request keyboard focus from compositor + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "sidebar" + + // State for search results + property var searchResults: [] + property var allApps: [] + property string currentQuery: "" + property bool isSearching: false + + // Background + Rectangle { + anchors.fill: parent + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + Common.Appearance.panelOpacity + ) + } + + // Content + ColumnLayout { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.medium + + // Search bar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.medium + anchors.rightMargin: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.small + + Text { + text: Common.Icons.icons.search + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.onSurfaceVariant + } + + TextInput { + id: searchInput + Layout.fillWidth: true + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + clip: true + + property string placeholderText: "Search applications..." + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: searchInput.placeholderText + font: searchInput.font + color: Common.Appearance.m3colors.onSurfaceVariant + visible: !searchInput.text && !searchInput.activeFocus + } + + onTextChanged: { + root.currentQuery = text + root.isSearching = text.trim() !== "" + if (root.isSearching) { + queryDebounceTimer.restart() + } else { + searchResults = [] + } + } + + Keys.onEscapePressed: { + if (text !== "") { + text = "" + } else { + Root.GlobalStates.sidebarLeftOpen = false + } + } + + Keys.onDownPressed: appListView.incrementCurrentIndex() + Keys.onUpPressed: appListView.decrementCurrentIndex() + Keys.onReturnPressed: { + if (appListView.currentIndex >= 0 && appListView.currentIndex < appListView.count) { + const apps = root.isSearching ? searchResults : allApps + launchApp(apps[appListView.currentIndex]) + } + } + } + + // Clear button + MouseArea { + visible: searchInput.text !== "" + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + cursorShape: Qt.PointingHandCursor + onClicked: searchInput.text = "" + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconSmall + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + } + + // App grid/list + ListView { + id: appListView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 2 + + model: root.isSearching ? searchResults : allApps + + delegate: MouseArea { + id: appDelegate + required property var modelData + required property int index + + width: appListView.width + height: 48 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: launchApp(modelData) + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.medium + color: appDelegate.containsMouse || appListView.currentIndex === appDelegate.index + ? Common.Appearance.m3colors.surfaceVariant + : "transparent" + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.medium + anchors.rightMargin: Common.Appearance.spacing.medium + spacing: Common.Appearance.spacing.medium + + // App icon with cascading fallback + Item { + id: iconContainer + Layout.preferredWidth: 32 + Layout.preferredHeight: 32 + + property string iconName: modelData._raw ? modelData._raw.icon : "" + property bool iconLoaded: primaryIcon.status === Image.Ready || flatpakIcon.status === Image.Ready + + // Primary: Qt icon provider + Image { + id: primaryIcon + anchors.fill: parent + source: iconContainer.iconName ? "image://icon/" + iconContainer.iconName : "" + sourceSize: Qt.size(32, 32) + smooth: true + visible: status === Image.Ready + } + + // Fallback 1: Flatpak icon path + Image { + id: flatpakIcon + anchors.fill: parent + source: primaryIcon.status === Image.Error && iconContainer.iconName + ? "file:///var/lib/flatpak/exports/share/icons/hicolor/128x128/apps/" + iconContainer.iconName + ".png" + : "" + sourceSize: Qt.size(32, 32) + smooth: true + visible: primaryIcon.status !== Image.Ready && status === Image.Ready + } + + // Fallback 2: Letter icon + Rectangle { + anchors.fill: parent + visible: !iconContainer.iconLoaded + radius: Common.Appearance.rounding.small + color: Common.Appearance.m3colors.primaryContainer + + Text { + anchors.centerIn: parent + text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" + font.pixelSize: 16 + font.bold: true + color: Common.Appearance.m3colors.onPrimaryContainer + } + } + } + + // App info + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + Layout.fillWidth: true + text: modelData.name || "Unknown" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + elide: Text.ElideRight + } + + Text { + Layout.fillWidth: true + visible: text !== "" && text !== modelData.name + text: modelData.description || modelData.genericName || "" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + elide: Text.ElideRight + } + } + } + } + + // Empty state + Text { + anchors.centerIn: parent + visible: appListView.count === 0 + text: root.isSearching ? "No applications found" : "Loading applications..." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + } + } + + // Track the query we're waiting for results from + property string activeQueryId: "" + + // Load all apps on startup + Component.onCompleted: { + allAppsQuery.running = true + } + + // Query to get all applications sorted alphabetically + Process { + id: allAppsQuery + property var pendingApps: [] + command: ["bash", "-lc", "datacube-cli query '' --json -m 500 -p applications"] + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + if (!data || data.trim() === "") return + + try { + const item = JSON.parse(data) + const result = { + id: item.id || "", + type: "app", + name: item.text || "", + description: item.subtext || "", + genericName: item.subtext || "", + icon: getIconPath(item.icon), + exec: item.exec || "", + provider: item.provider || "", + score: item.score || 0, + _raw: item + } + allAppsQuery.pendingApps.push(result) + } catch (e) { + console.log("Failed to parse app:", e, data) + } + } + } + + onStarted: { + allAppsQuery.pendingApps = [] + } + + onExited: { + // Sort alphabetically by name + allAppsQuery.pendingApps.sort((a, b) => { + const nameA = (a.name || "").toLowerCase() + const nameB = (b.name || "").toLowerCase() + if (nameA < nameB) return -1 + if (nameA > nameB) return 1 + return 0 + }) + root.allApps = allAppsQuery.pendingApps + } + } + + // Debounce timer for search queries + Timer { + id: queryDebounceTimer + interval: 150 + onTriggered: { + const query = root.currentQuery + if (!query || query.trim() === "") { + searchResults = [] + root.activeQueryId = "" + return + } + // Generate a unique ID for this query + root.activeQueryId = query + "_" + Date.now() + datacubeQuery.queryId = root.activeQueryId + datacubeQuery.query = query + datacubeQuery.running = true + } + } + + // Datacube query process + Process { + id: datacubeQuery + property string query: "" + property string queryId: "" + property var pendingResults: [] + command: ["bash", "-lc", "datacube-cli query '" + query.replace(/'/g, "'\\''") + "' --json -m 50"] + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + if (!data || data.trim() === "") return + + try { + const item = JSON.parse(data) + const result = { + id: item.id || "", + type: item.provider === "applications" ? "app" : item.provider, + name: item.text || "", + description: item.subtext || "", + genericName: item.subtext || "", + icon: getIconPath(item.icon), + exec: item.exec || "", + provider: item.provider || "", + score: item.score || 0, + _raw: item + } + datacubeQuery.pendingResults.push(result) + } catch (e) { + console.log("Failed to parse datacube result:", e, data) + } + } + } + + onStarted: { + datacubeQuery.pendingResults = [] + } + + onExited: { + // Only update results if this is still the active query + if (datacubeQuery.queryId === root.activeQueryId) { + root.searchResults = datacubeQuery.pendingResults + } + } + } + + // Datacube activate process + Process { + id: datacubeActivate + property string itemJson: "" + command: ["bash", "-lc", "echo '" + itemJson + "' | datacube-cli activate --json"] + } + + function getIconPath(iconName) { + if (!iconName) return "" + if (iconName.startsWith("/")) return "file://" + iconName + // Try Qt icon provider first + return "image://icon/" + iconName + } + + // Try to find icon in flatpak exports if Qt icon provider fails + function getFlatpakIconPath(iconName, size) { + if (!iconName) return "" + const sizes = [size || 128, 64, 48, 32, 256, 512] + const basePath = "/var/lib/flatpak/exports/share/icons/hicolor/" + + for (const s of sizes) { + const path = basePath + s + "x" + s + "/apps/" + iconName + ".png" + return "file://" + path + } + return "" + } + + function launchApp(app) { + if (!app) return + + if (app._raw) { + const itemJson = JSON.stringify(app._raw).replace(/'/g, "'\\''") + datacubeActivate.itemJson = itemJson + datacubeActivate.running = true + } else if (app.exec) { + appLaunchProcess.command = ["sh", "-c", app.exec] + appLaunchProcess.running = true + } + + Root.GlobalStates.sidebarLeftOpen = false + searchInput.text = "" + } + + // Fallback app launcher process + Process { + id: appLaunchProcess + command: ["true"] + } + + // Focus search when sidebar opens + onVisibleChanged: { + if (visible) { + searchInput.forceActiveFocus() + appListView.currentIndex = 0 + } else { + searchInput.text = "" + searchResults = [] + } + } +} diff --git a/dot_files/quickshell/modules/sidebars/SidebarRight.qml b/dot_files/quickshell/modules/sidebars/SidebarRight.qml new file mode 100644 index 0000000..82fcdd5 --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/SidebarRight.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import Quickshell.Wayland + +import "../common" as Common +import "../../services" as Services +import "../../" as Root + +PanelWindow { + id: root + + required property var targetScreen + screen: targetScreen + + anchors { + top: true + bottom: true + right: true + } + + margins.top: Common.Appearance.sizes.barHeight + + implicitWidth: Common.Appearance.sizes.sidebarWidth + color: "transparent" + + visible: Root.GlobalStates.sidebarRightOpen + + // Don't grab keyboard focus - let bar remain clickable + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "sidebar" + + // Background + Rectangle { + anchors.fill: parent + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + Common.Appearance.panelOpacity + ) + } + + // Bluetooth View (shown when sidebarRightView === "bluetooth") + Loader { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + active: Root.GlobalStates.sidebarRightView === "bluetooth" + source: "BluetoothView.qml" + } + + // Audio View (shown when sidebarRightView === "audio") + Loader { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + active: Root.GlobalStates.sidebarRightView === "audio" + source: "AudioView.qml" + } + + // Calendar View (shown when sidebarRightView === "calendar") + Loader { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + active: Root.GlobalStates.sidebarRightView === "calendar" + source: "CalendarView.qml" + } + + // Notifications View (shown when sidebarRightView === "notifications") + Loader { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + active: Root.GlobalStates.sidebarRightView === "notifications" + source: "NotificationsView.qml" + } + + // Default Content (shown when sidebarRightView === "default") + Loader { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + active: Root.GlobalStates.sidebarRightView === "default" + source: "DefaultView.qml" + } +} diff --git a/dot_files/quickshell/modules/sidebars/qmldir b/dot_files/quickshell/modules/sidebars/qmldir new file mode 100644 index 0000000..9407dea --- /dev/null +++ b/dot_files/quickshell/modules/sidebars/qmldir @@ -0,0 +1,4 @@ +module sidebars + +SidebarLeft 1.0 SidebarLeft.qml +SidebarRight 1.0 SidebarRight.qml diff --git a/dot_files/quickshell/modules/switcher/AppSwitcher.qml b/dot_files/quickshell/modules/switcher/AppSwitcher.qml new file mode 100644 index 0000000..03efbd0 --- /dev/null +++ b/dot_files/quickshell/modules/switcher/AppSwitcher.qml @@ -0,0 +1,267 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +import "../common" as Common +import "../../services" as Services + +// App switcher overlay - centered on screen +PanelWindow { + id: root + + required property var targetScreen + screen: targetScreen + + // Center on screen + anchors { + top: true + bottom: true + left: true + right: true + } + + color: "transparent" + + visible: Services.Windows.switcherActive + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.namespace: "appswitcher" + + // Focus scope for keyboard handling + FocusScope { + id: focusRoot + anchors.fill: parent + focus: true + + // Keyboard handling + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Tab) { + if (event.modifiers & Qt.ShiftModifier) { + Services.Windows.prevWindow() + } else { + Services.Windows.nextWindow() + } + event.accepted = true + } else if (event.key === Qt.Key_Escape) { + Services.Windows.cancelSwitcher() + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + Services.Windows.selectWindow() + event.accepted = true + } else if (event.key === Qt.Key_Left) { + Services.Windows.prevWindow() + event.accepted = true + } else if (event.key === Qt.Key_Right) { + Services.Windows.nextWindow() + event.accepted = true + } + } + + Keys.onReleased: (event) => { + // When Super (Meta) is released, select the current window + if (event.key === Qt.Key_Super_L || event.key === Qt.Key_Super_R || event.key === Qt.Key_Meta) { + Services.Windows.selectWindow() + event.accepted = true + } + } + + // Dark overlay background + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.5) + + MouseArea { + anchors.fill: parent + onClicked: Services.Windows.cancelSwitcher() + } + } + } + + // Switcher panel - centered + Rectangle { + id: switcherPanel + anchors.centerIn: parent + + // Calculate width based on number of windows + readonly property int itemWidth: 120 + readonly property int itemSpacing: Common.Appearance.spacing.medium + readonly property int windowCount: Services.Windows.windows.length + readonly property int contentWidth: windowCount > 0 + ? (windowCount * itemWidth) + ((windowCount - 1) * itemSpacing) + : 200 // Minimum width when no windows + + width: Math.min(parent.width * 0.8, contentWidth + Common.Appearance.spacing.large * 2) + height: 160 + radius: Common.Appearance.rounding.large + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + 0.95 + ) + + // Window list + ListView { + id: switcherRow + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + orientation: ListView.Horizontal + spacing: switcherPanel.itemSpacing + clip: true + + model: Services.Windows.windows + currentIndex: Services.Windows.currentIndex + + highlightFollowsCurrentItem: true + highlightMoveDuration: 150 + + delegate: Item { + id: windowDelegate + required property var modelData + required property int index + + width: switcherPanel.itemWidth + height: switcherRow.height + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.medium + color: switcherRow.currentIndex === windowDelegate.index + ? Common.Appearance.m3colors.primaryContainer + : (delegateMouse.containsMouse + ? Common.Appearance.m3colors.surfaceVariant + : "transparent") + + Behavior on color { + ColorAnimation { duration: 100 } + } + } + + MouseArea { + id: delegateMouse + anchors.fill: parent + hoverEnabled: true + onClicked: { + Services.Windows.currentIndex = windowDelegate.index + Services.Windows.selectWindow() + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.small + spacing: Common.Appearance.spacing.tiny + + // App icon + Item { + Layout.fillWidth: true + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignHCenter + + // Get icon from IconResolver service + property string cachedIcon: modelData.class ? Services.IconResolver.getIcon(modelData.class) : "" + + // Primary: datacube cached icon + Image { + id: appIcon + anchors.centerIn: parent + width: 48 + height: 48 + source: parent.cachedIcon + sourceSize: Qt.size(48, 48) + smooth: true + visible: status === Image.Ready + } + + // Fallback letter icon + Rectangle { + anchors.centerIn: parent + width: 48 + height: 48 + visible: appIcon.status !== Image.Ready + radius: Common.Appearance.rounding.medium + color: Common.Appearance.m3colors.secondaryContainer + + Text { + anchors.centerIn: parent + text: modelData.class ? modelData.class.charAt(0).toUpperCase() : "?" + font.pixelSize: 20 + font.bold: true + color: Common.Appearance.m3colors.onSecondaryContainer + } + } + } + + // Window title + Text { + Layout.fillWidth: true + Layout.fillHeight: true + text: modelData.title || modelData.class || "Window" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: switcherRow.currentIndex === windowDelegate.index + ? Common.Appearance.m3colors.onPrimaryContainer + : Common.Appearance.m3colors.onSurface + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + } + + // Workspace indicator + Text { + Layout.fillWidth: true + text: "Workspace " + (modelData.workspace ? modelData.workspace.id : "?") + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small - 2 + color: Common.Appearance.m3colors.onSurfaceVariant + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + + // Current window title at bottom + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: switcherPanel.bottom + anchors.topMargin: Common.Appearance.spacing.medium + width: titleText.implicitWidth + Common.Appearance.spacing.large * 2 + height: titleText.implicitHeight + Common.Appearance.spacing.medium + radius: Common.Appearance.rounding.medium + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + 0.95 + ) + visible: Services.Windows.windows.length > 0 + + Text { + id: titleText + anchors.centerIn: parent + text: { + const windows = Services.Windows.windows + const idx = Services.Windows.currentIndex + if (windows.length > 0 && idx < windows.length) { + return windows[idx].title || windows[idx].class || "" + } + return "" + } + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + maximumLineCount: 1 + elide: Text.ElideMiddle + } + } + + onVisibleChanged: { + if (visible) { + focusRoot.forceActiveFocus() + } + } +} diff --git a/dot_files/quickshell/modules/switcher/qmldir b/dot_files/quickshell/modules/switcher/qmldir new file mode 100644 index 0000000..8fd8d07 --- /dev/null +++ b/dot_files/quickshell/modules/switcher/qmldir @@ -0,0 +1,3 @@ +module Switcher + +AppSwitcher 1.0 AppSwitcher.qml diff --git a/dot_files/quickshell/modules/welcome/Welcome.qml b/dot_files/quickshell/modules/welcome/Welcome.qml new file mode 100644 index 0000000..06aa9ba --- /dev/null +++ b/dot_files/quickshell/modules/welcome/Welcome.qml @@ -0,0 +1,634 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Io + +import "../common" as Common + +// First-boot welcome wizard for user creation and initial setup +PanelWindow { + id: root + + required property var screen + + // Full screen overlay + anchors.fill: true + + color: Common.Appearance.m3colors.background + + visible: false // Controlled externally + + property int currentStep: 0 + property int totalSteps: 4 + + // User creation data + property string username: "" + property string fullName: "" + property string password: "" + property string passwordConfirm: "" + property string errorMessage: "" + + // Theme preferences + property bool selectedDarkMode: true + property string selectedAccent: "blue" + + ColumnLayout { + anchors.centerIn: parent + width: Math.min(parent.width - 100, 500) + spacing: Common.Appearance.spacing.xlarge + + // Header + ColumnLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Text { + Layout.alignment: Qt.AlignHCenter + text: "Welcome to Hypercube" + font.family: Common.Appearance.fonts.title + font.pixelSize: Common.Appearance.fontSize.display + font.weight: Font.Medium + color: Common.Appearance.m3colors.primary + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: getStepTitle() + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.large + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + // Progress indicator + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Common.Appearance.spacing.small + + Repeater { + model: totalSteps + + Rectangle { + width: 40 + height: 4 + radius: 2 + color: index <= currentStep + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.surfaceVariant + } + } + } + + // Content area + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 320 + radius: Common.Appearance.rounding.xlarge + color: Common.Appearance.m3colors.surfaceVariant + + Loader { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.xlarge + sourceComponent: { + switch (currentStep) { + case 0: return usernameStep + case 1: return passwordStep + case 2: return themeStep + case 3: return confirmStep + default: return usernameStep + } + } + } + } + + // Error message + Text { + Layout.alignment: Qt.AlignHCenter + visible: errorMessage !== "" + text: errorMessage + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.error + } + + // Navigation buttons + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.medium + + // Back button + MouseArea { + visible: currentStep > 0 + Layout.preferredWidth: 100 + Layout.preferredHeight: 44 + cursorShape: Qt.PointingHandCursor + onClicked: { + errorMessage = "" + currentStep-- + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.large + color: parent.containsMouse + ? Common.Appearance.m3colors.surfaceVariant + : "transparent" + border.width: 1 + border.color: Common.Appearance.m3colors.outline + } + + Text { + anchors.centerIn: parent + text: "Back" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + } + } + + Item { Layout.fillWidth: true } + + // Next/Create button + MouseArea { + Layout.preferredWidth: currentStep === totalSteps - 1 ? 160 : 100 + Layout.preferredHeight: 44 + cursorShape: Qt.PointingHandCursor + onClicked: { + if (validateStep()) { + if (currentStep === totalSteps - 1) { + createUser() + } else { + currentStep++ + } + } + } + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.large + color: parent.containsMouse + ? Qt.darker(Common.Appearance.m3colors.primary, 1.1) + : Common.Appearance.m3colors.primary + } + + Text { + anchors.centerIn: parent + text: currentStep === totalSteps - 1 ? "Create Account" : "Next" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onPrimary + } + } + } + } + + // Step 1: Username + Component { + id: usernameStep + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + Text { + Layout.fillWidth: true + text: "Let's create your account. First, choose a username." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + wrapMode: Text.WordWrap + } + + WizardInput { + id: usernameInput + Layout.fillWidth: true + label: "Username" + placeholder: "Enter username" + text: username + onTextChanged: username = text + + Component.onCompleted: forceActiveFocus() + } + + WizardInput { + Layout.fillWidth: true + label: "Full Name (optional)" + placeholder: "Enter your full name" + text: fullName + onTextChanged: fullName = text + } + + Text { + Layout.fillWidth: true + text: "Username must be lowercase, start with a letter, and contain only letters, numbers, underscores, or hyphens." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + wrapMode: Text.WordWrap + } + } + } + + // Step 2: Password + Component { + id: passwordStep + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + Text { + Layout.fillWidth: true + text: "Create a secure password for your account." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + wrapMode: Text.WordWrap + } + + WizardInput { + id: passwordInput + Layout.fillWidth: true + label: "Password" + placeholder: "Enter password" + isPassword: true + text: password + onTextChanged: password = text + + Component.onCompleted: forceActiveFocus() + } + + WizardInput { + Layout.fillWidth: true + label: "Confirm Password" + placeholder: "Confirm password" + isPassword: true + text: passwordConfirm + onTextChanged: passwordConfirm = text + } + + Text { + Layout.fillWidth: true + text: "Password must be at least 8 characters." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + wrapMode: Text.WordWrap + } + } + } + + // Step 3: Theme selection + Component { + id: themeStep + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + Text { + Layout.fillWidth: true + text: "Choose your preferred appearance. You can change this later in Settings." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + wrapMode: Text.WordWrap + } + + // Dark/Light mode + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.medium + + ThemeOption { + Layout.fillWidth: true + label: "Dark" + icon: Common.Icons.icons.night + selected: selectedDarkMode + onClicked: selectedDarkMode = true + } + + ThemeOption { + Layout.fillWidth: true + label: "Light" + icon: Common.Icons.icons.sunny + selected: !selectedDarkMode + onClicked: selectedDarkMode = false + } + } + + Text { + Layout.topMargin: Common.Appearance.spacing.medium + text: "Accent Color" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurfaceVariant + } + + // Accent colors + RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.small + + Repeater { + model: [ + { id: "blue", color: "#7aa2f7" }, + { id: "green", color: "#9ece6a" }, + { id: "purple", color: "#bb9af7" }, + { id: "orange", color: "#ff9e64" }, + { id: "red", color: "#f7768e" }, + { id: "cyan", color: "#7dcfff" } + ] + + delegate: MouseArea { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + cursorShape: Qt.PointingHandCursor + onClicked: selectedAccent = modelData.id + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.full + color: modelData.color + + Rectangle { + anchors.centerIn: parent + width: 20 + height: 20 + radius: 10 + visible: selectedAccent === modelData.id + color: "white" + + Text { + anchors.centerIn: parent + text: Common.Icons.icons.check + font.family: Common.Appearance.fonts.icon + font.pixelSize: 14 + color: modelData.color + } + } + } + } + } + } + } + } + + // Step 4: Confirmation + Component { + id: confirmStep + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + Text { + Layout.fillWidth: true + text: "Review your settings and create your account." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + wrapMode: Text.WordWrap + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: summaryColumn.implicitHeight + Common.Appearance.spacing.large * 2 + radius: Common.Appearance.rounding.large + color: Common.Appearance.surfaceLayer(1) + + ColumnLayout { + id: summaryColumn + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.large + spacing: Common.Appearance.spacing.medium + + SummaryRow { label: "Username"; value: username } + SummaryRow { label: "Full Name"; value: fullName || "(not set)" } + SummaryRow { label: "Theme"; value: selectedDarkMode ? "Dark" : "Light" } + SummaryRow { label: "Accent"; value: selectedAccent.charAt(0).toUpperCase() + selectedAccent.slice(1) } + } + } + + Text { + Layout.fillWidth: true + text: "Your account will be added to the 'wheel' group for administrator access." + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + wrapMode: Text.WordWrap + } + } + } + + // Helper components + component WizardInput: ColumnLayout { + property string label: "" + property string placeholder: "" + property bool isPassword: false + property alias text: inputField.text + + signal textChanged() + + spacing: Common.Appearance.spacing.tiny + + function forceActiveFocus() { + inputField.forceActiveFocus() + } + + Text { + text: label + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurfaceVariant + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: Common.Appearance.rounding.medium + color: Common.Appearance.surfaceLayer(1) + border.width: inputField.activeFocus ? 2 : 1 + border.color: inputField.activeFocus + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.outline + + TextInput { + id: inputField + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.medium + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + echoMode: isPassword ? TextInput.Password : TextInput.Normal + clip: true + + onTextChanged: parent.parent.textChanged() + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: placeholder + font: inputField.font + color: Common.Appearance.m3colors.onSurfaceVariant + visible: !inputField.text && !inputField.activeFocus + } + } + } + } + + component ThemeOption: MouseArea { + property string label: "" + property string icon: "" + property bool selected: false + + Layout.preferredHeight: 80 + cursorShape: Qt.PointingHandCursor + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.large + color: selected + ? Common.Appearance.m3colors.primaryContainer + : Common.Appearance.surfaceLayer(1) + border.width: selected ? 2 : 1 + border.color: selected + ? Common.Appearance.m3colors.primary + : Common.Appearance.m3colors.outline + + ColumnLayout { + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Text { + Layout.alignment: Qt.AlignHCenter + text: icon + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconXLarge + color: selected + ? Common.Appearance.m3colors.onPrimaryContainer + : Common.Appearance.m3colors.onSurface + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: label + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: selected + ? Common.Appearance.m3colors.onPrimaryContainer + : Common.Appearance.m3colors.onSurface + } + } + } + } + + component SummaryRow: RowLayout { + property string label: "" + property string value: "" + + Layout.fillWidth: true + + Text { + text: label + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + + Item { Layout.fillWidth: true } + + Text { + text: value + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + } + } + + function getStepTitle(): string { + switch (currentStep) { + case 0: return "Create Your Account" + case 1: return "Set Your Password" + case 2: return "Choose Your Theme" + case 3: return "Confirm Your Settings" + default: return "" + } + } + + function validateStep(): bool { + errorMessage = "" + + switch (currentStep) { + case 0: + if (!username || username.trim() === "") { + errorMessage = "Username is required" + return false + } + if (!/^[a-z_][a-z0-9_-]*$/.test(username)) { + errorMessage = "Invalid username format" + return false + } + if (username.length > 32) { + errorMessage = "Username too long (max 32 characters)" + return false + } + return true + + case 1: + if (!password || password.length < 8) { + errorMessage = "Password must be at least 8 characters" + return false + } + if (password !== passwordConfirm) { + errorMessage = "Passwords do not match" + return false + } + return true + + case 2: + return true + + case 3: + return true + + default: + return true + } + } + + function createUser() { + errorMessage = "" + + // Create user via pkexec (polkit) + userCreateProcess.running = true + } + + Process { + id: userCreateProcess + command: ["pkexec", "sh", "-c", + "useradd -m -G wheel -c '" + (fullName || username) + "' '" + username + "' && " + + "echo '" + username + ":" + password + "' | chpasswd" + ] + + running: false + onExited: { + if (exitCode === 0) { + // Save theme preferences + Common.Config.darkMode = selectedDarkMode + Common.Config.accentColor = selectedAccent + Common.Config.save() + + // Hide wizard and signal completion + root.visible = false + userCreated() + } else { + errorMessage = "Failed to create user. Please try again." + } + } + } + + signal userCreated() +} diff --git a/dot_files/quickshell/modules/welcome/qmldir b/dot_files/quickshell/modules/welcome/qmldir new file mode 100644 index 0000000..2a3f848 --- /dev/null +++ b/dot_files/quickshell/modules/welcome/qmldir @@ -0,0 +1,3 @@ +module welcome + +Welcome 1.0 Welcome.qml diff --git a/dot_files/quickshell/services/Audio.qml b/dot_files/quickshell/services/Audio.qml new file mode 100644 index 0000000..3be266b --- /dev/null +++ b/dot_files/quickshell/services/Audio.qml @@ -0,0 +1,392 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Audio control via PipeWire/PulseAudio +Singleton { + id: root + + // Sink (output) properties + property real volume: 0.75 + property bool muted: false + property string sinkName: "" + property string sinkDescription: "" + property int defaultSinkId: -1 + + // Source (input) properties + property real micVolume: 1.0 + property bool micMuted: false + property string sourceName: "" + property string sourceDescription: "" + property int defaultSourceId: -1 + + // Available devices + property var sinks: [] // [{id, name, description, isDefault}] + property var sources: [] // [{id, name, description, isDefault}] + + // Active streams + property int activeStreams: 0 + + // Pending device lists during parsing + property var pendingSinks: [] + property var pendingSources: [] + property bool parsingSinks: false + property bool parsingSources: false + + // Device list query script (using pactl for reliable parsing) + readonly property string deviceListScript: " + if command -v pactl &>/dev/null; then + # Get default sink and source names + DEFAULT_SINK=$(pactl get-default-sink 2>/dev/null) + DEFAULT_SOURCE=$(pactl get-default-source 2>/dev/null) + + # List sinks + echo \"sinks_start\" + pactl list sinks 2>/dev/null | awk ' + /^Sink #/ { id = substr($2, 2) } + /^\\tName:/ { name = $2 } + /^\\tDescription:/ { + desc = substr($0, index($0, \":\")+2) + print \"sink|\" id \"|\" name \"|\" desc + } + ' | while IFS='|' read -r type id name desc; do + IS_DEFAULT=\"false\" + if [ \"$name\" = \"$DEFAULT_SINK\" ]; then + IS_DEFAULT=\"true\" + fi + echo \"sink|$id|$name|$desc|$IS_DEFAULT\" + done + echo \"sinks_end\" + + # List sources (exclude monitors) + echo \"sources_start\" + pactl list sources 2>/dev/null | awk ' + /^Source #/ { id = substr($2, 2) } + /^\\tName:/ { name = $2 } + /^\\tDescription:/ { + desc = substr($0, index($0, \":\")+2) + print \"source|\" id \"|\" name \"|\" desc + } + ' | grep -v \"Monitor of\" | while IFS='|' read -r type id name desc; do + IS_DEFAULT=\"false\" + if [ \"$name\" = \"$DEFAULT_SOURCE\" ]; then + IS_DEFAULT=\"true\" + fi + echo \"source|$id|$name|$desc|$IS_DEFAULT\" + done + echo \"sources_end\" + fi + " + + // Audio query script + readonly property string audioQueryScript: " + if command -v wpctl &>/dev/null; then + SINK_VOL=$(wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null) + if [ -n \"$SINK_VOL\" ]; then + VOL=$(echo \"$SINK_VOL\" | awk '{print $2}') + echo \"volume=$VOL\" + if echo \"$SINK_VOL\" | grep -q \"MUTED\"; then + echo \"muted=true\" + else + echo \"muted=false\" + fi + fi + + SOURCE_VOL=$(wpctl get-volume @DEFAULT_AUDIO_SOURCE@ 2>/dev/null) + if [ -n \"$SOURCE_VOL\" ]; then + VOL=$(echo \"$SOURCE_VOL\" | awk '{print $2}') + echo \"micVolume=$VOL\" + if echo \"$SOURCE_VOL\" | grep -q \"MUTED\"; then + echo \"micMuted=true\" + else + echo \"micMuted=false\" + fi + fi + + SINK_INFO=$(wpctl inspect @DEFAULT_AUDIO_SINK@ 2>/dev/null | grep \"node.description\" | head -1 | cut -d'\"' -f2) + echo \"sinkDescription=$SINK_INFO\" + + elif command -v pactl &>/dev/null; then + SINK=$(pactl get-default-sink 2>/dev/null) + if [ -n \"$SINK\" ]; then + VOL=$(pactl get-sink-volume \"$SINK\" 2>/dev/null | head -1 | sed 's/.*\\/ *\\([0-9]*\\)%.*/\\1/') + echo \"volume=$(echo \"scale=2; $VOL/100\" | bc)\" + + MUTE=$(pactl get-sink-mute \"$SINK\" 2>/dev/null | grep -c \"yes\") + if [ \"$MUTE\" = \"1\" ]; then + echo \"muted=true\" + else + echo \"muted=false\" + fi + fi + + SOURCE=$(pactl get-default-source 2>/dev/null) + if [ -n \"$SOURCE\" ]; then + VOL=$(pactl get-source-volume \"$SOURCE\" 2>/dev/null | head -1 | sed 's/.*\\/ *\\([0-9]*\\)%.*/\\1/') + echo \"micVolume=$(echo \"scale=2; $VOL/100\" | bc)\" + + MUTE=$(pactl get-source-mute \"$SOURCE\" 2>/dev/null | grep -c \"yes\") + if [ \"$MUTE\" = \"1\" ]; then + echo \"micMuted=true\" + else + echo \"micMuted=false\" + fi + fi + fi + " + + // Run initial device query on startup + Component.onCompleted: { + deviceListProcess.running = true + } + + Process { + id: audioProcess + command: ["sh", "-c", root.audioQueryScript] + running: true + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => root.parseVolumeLine(data) + } + } + + Process { + id: deviceListProcess + command: ["sh", "-c", root.deviceListScript] + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => root.parseDeviceLine(data) + } + + onRunningChanged: { + if (running) { + root.pendingSinks = [] + root.pendingSources = [] + root.parsingSinks = false + root.parsingSources = false + } + } + + onExited: { + root.sinks = root.pendingSinks + root.sources = root.pendingSources + } + } + + function parseVolumeLine(line) { + if (!line || line.trim() === "") return + + const idx = line.indexOf("=") + if (idx === -1) return + + const key = line.substring(0, idx).trim() + const value = line.substring(idx + 1).trim() + + switch (key) { + case "volume": + volume = parseFloat(value) || 0.75 + break + case "muted": + muted = value === "true" + break + case "micVolume": + micVolume = parseFloat(value) || 1.0 + break + case "micMuted": + micMuted = value === "true" + break + case "sinkName": + sinkName = value + break + case "sinkDescription": + sinkDescription = value + break + case "sourceName": + sourceName = value + break + case "sourceDescription": + sourceDescription = value + break + } + } + + function parseDeviceLine(line) { + if (!line || line.trim() === "") return + + // Parse device list markers + if (line === "sinks_start") { + parsingSinks = true + parsingSources = false + return + } + if (line === "sinks_end") { + parsingSinks = false + return + } + if (line === "sources_start") { + parsingSources = true + parsingSinks = false + return + } + if (line === "sources_end") { + parsingSources = false + return + } + + // Parse sink entries: sink|id|name|description|isDefault + if (parsingSinks && line.startsWith("sink|")) { + const parts = line.split("|") + if (parts.length >= 5) { + pendingSinks.push({ + id: parseInt(parts[1]), + name: parts[2], + description: parts[3], + isDefault: parts[4] === "true" + }) + } + return + } + + // Parse source entries: source|id|name|description|isDefault + if (parsingSources && line.startsWith("source|")) { + const parts = line.split("|") + if (parts.length >= 5) { + pendingSources.push({ + id: parseInt(parts[1]), + name: parts[2], + description: parts[3], + isDefault: parts[4] === "true" + }) + } + return + } + + // Parse default IDs + const idx = line.indexOf("=") + if (idx === -1) return + + const key = line.substring(0, idx).trim() + const value = line.substring(idx + 1).trim() + + switch (key) { + case "defaultSinkId": + defaultSinkId = parseInt(value) || -1 + break + case "defaultSourceId": + defaultSourceId = parseInt(value) || -1 + break + } + } + + // Update timer for volume (fast) + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: audioProcess.running = true + } + + // Update timer for device list (slower) + Timer { + interval: 5000 + running: true + repeat: true + onTriggered: deviceListProcess.running = true + } + + // Control functions + function setVolume(value) { + const percent = Math.round(value * 100) + volumeProcess.command = ["sh", "-c", "wpctl set-volume @DEFAULT_AUDIO_SINK@ " + percent + "% || pactl set-sink-volume @DEFAULT_SINK@ " + percent + "%"] + volumeProcess.running = true + volume = value + } + + Process { + id: volumeProcess + command: ["true"] + } + + function setMuted(mute) { + const state = mute ? "1" : "0" + muteProcess.command = ["sh", "-c", "wpctl set-mute @DEFAULT_AUDIO_SINK@ " + state + " || pactl set-sink-mute @DEFAULT_SINK@ " + (mute ? "yes" : "no")] + muteProcess.running = true + muted = mute + } + + Process { + id: muteProcess + command: ["true"] + } + + function toggleMute() { + setMuted(!muted) + } + + function setMicVolume(value) { + const percent = Math.round(value * 100) + micVolumeProcess.command = ["sh", "-c", "wpctl set-volume @DEFAULT_AUDIO_SOURCE@ " + percent + "% || pactl set-source-volume @DEFAULT_SOURCE@ " + percent + "%"] + micVolumeProcess.running = true + micVolume = value + } + + Process { + id: micVolumeProcess + command: ["true"] + } + + function setMicMuted(mute) { + const state = mute ? "1" : "0" + micMuteProcess.command = ["sh", "-c", "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ " + state + " || pactl set-source-mute @DEFAULT_SOURCE@ " + (mute ? "yes" : "no")] + micMuteProcess.running = true + micMuted = mute + } + + Process { + id: micMuteProcess + command: ["true"] + } + + function toggleMicMute() { + setMicMuted(!micMuted) + } + + function setDefaultSink(name) { + setSinkProcess.command = ["pactl", "set-default-sink", name] + setSinkProcess.running = true + } + + Process { + id: setSinkProcess + command: ["true"] + onExited: deviceListProcess.running = true + } + + function setDefaultSource(name) { + setSourceProcess.command = ["pactl", "set-default-source", name] + setSourceProcess.running = true + } + + Process { + id: setSourceProcess + command: ["true"] + onExited: deviceListProcess.running = true + } + + function refreshDevices() { + deviceListProcess.running = true + } + + // Volume as percentage + function volumePercent() { + return Math.round(volume * 100) + } + + function micVolumePercent() { + return Math.round(micVolume * 100) + } +} diff --git a/dot_files/quickshell/services/Battery.qml b/dot_files/quickshell/services/Battery.qml new file mode 100644 index 0000000..fb3fb74 --- /dev/null +++ b/dot_files/quickshell/services/Battery.qml @@ -0,0 +1,164 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property bool present: false + property int percent: 100 + property bool charging: false + property bool pluggedIn: false + property string status: "Unknown" + property int timeToEmpty: 0 // minutes + property int timeToFull: 0 // minutes + + // Battery health + property int designCapacity: 0 + property int fullCapacity: 0 + property real health: 1.0 + + // Low battery threshold + readonly property int lowThreshold: 20 + readonly property int criticalThreshold: 10 + + property bool isLow: percent <= lowThreshold && !charging + property bool isCritical: percent <= criticalThreshold && !charging + + // Battery query script + readonly property string batteryQueryScript: " + BAT_PATH=\"\" + for bat in /sys/class/power_supply/BAT*; do + if [ -d \"$bat\" ]; then + BAT_PATH=\"$bat\" + break + fi + done + + if [ -z \"$BAT_PATH\" ]; then + echo \"present=false\" + exit 0 + fi + + echo \"present=true\" + + if [ -f \"$BAT_PATH/capacity\" ]; then + echo \"percent=$(cat $BAT_PATH/capacity)\" + fi + + if [ -f \"$BAT_PATH/status\" ]; then + STATUS=$(cat $BAT_PATH/status) + echo \"status=$STATUS\" + case \"$STATUS\" in + Charging) echo \"charging=true\" ;; + *) echo \"charging=false\" ;; + esac + fi + + for ac in /sys/class/power_supply/AC* /sys/class/power_supply/ADP*; do + if [ -f \"$ac/online\" ]; then + if [ \"$(cat $ac/online)\" = \"1\" ]; then + echo \"pluggedIn=true\" + else + echo \"pluggedIn=false\" + fi + break + fi + done + + if [ -f \"$BAT_PATH/energy_now\" ] && [ -f \"$BAT_PATH/power_now\" ]; then + ENERGY=$(cat $BAT_PATH/energy_now) + POWER=$(cat $BAT_PATH/power_now) + if [ \"$POWER\" -gt 0 ]; then + HOURS=$(echo \"scale=2; $ENERGY / $POWER\" | bc 2>/dev/null || echo \"0\") + MINUTES=$(echo \"scale=0; $HOURS * 60\" | bc 2>/dev/null || echo \"0\") + echo \"timeRemaining=$MINUTES\" + fi + fi + + if [ -f \"$BAT_PATH/energy_full_design\" ] && [ -f \"$BAT_PATH/energy_full\" ]; then + DESIGN=$(cat $BAT_PATH/energy_full_design) + FULL=$(cat $BAT_PATH/energy_full) + echo \"designCapacity=$DESIGN\" + echo \"fullCapacity=$FULL\" + fi + " + + // UPower D-Bus would be ideal, but we'll use /sys/class/power_supply + Process { + id: batteryProcess + command: ["sh", "-c", root.batteryQueryScript] + running: true + onExited: root.parseOutput() + } + + function parseOutput() { + const output = batteryProcess.stdout + if (!output) return + const lines = output.split("\n") + + for (const line of lines) { + const [key, value] = line.split("=") + if (!key || !value) continue + + switch (key.trim()) { + case "present": + present = value.trim() === "true" + break + case "percent": + percent = parseInt(value.trim()) || 100 + break + case "charging": + charging = value.trim() === "true" + break + case "pluggedIn": + pluggedIn = value.trim() === "true" + break + case "status": + status = value.trim() + break + case "timeRemaining": + const mins = parseInt(value.trim()) || 0 + if (charging) { + timeToFull = mins + } else { + timeToEmpty = mins + } + break + case "designCapacity": + designCapacity = parseInt(value.trim()) || 0 + break + case "fullCapacity": + fullCapacity = parseInt(value.trim()) || 0 + if (designCapacity > 0) { + health = fullCapacity / designCapacity + } + break + } + } + } + + // Update timer + Timer { + interval: 30000 // 30 seconds + running: true + repeat: true + onTriggered: batteryProcess.running = true + } + + // Time remaining as string + function timeRemainingString(): string { + const mins = charging ? timeToFull : timeToEmpty + if (mins <= 0) return "" + + const hours = Math.floor(mins / 60) + const minutes = mins % 60 + + if (hours > 0) { + return hours + "h " + minutes + "m" + } + return minutes + "m" + } +} diff --git a/dot_files/quickshell/services/BluetoothStatus.qml b/dot_files/quickshell/services/BluetoothStatus.qml new file mode 100644 index 0000000..b13afeb --- /dev/null +++ b/dot_files/quickshell/services/BluetoothStatus.qml @@ -0,0 +1,294 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property bool available: false + property bool powered: false + property bool discovering: false + property bool connected: false + property string connectedDeviceName: "" + + property var devices: [] // List of paired devices + property var availableDevices: [] // List of discovered (not paired) devices + + // Bluetooth query script + readonly property string btQueryScript: " + if ! command -v bluetoothctl &>/dev/null; then + echo \"available=false\" + exit 0 + fi + + echo \"available=true\" + + CTRL=$(bluetoothctl show 2>/dev/null) + if [ -z \"$CTRL\" ]; then + echo \"powered=false\" + exit 0 + fi + + if echo \"$CTRL\" | grep -q \"Powered: yes\"; then + echo \"powered=true\" + else + echo \"powered=false\" + fi + + if echo \"$CTRL\" | grep -q \"Discovering: yes\"; then + echo \"discovering=true\" + else + echo \"discovering=false\" + fi + + CONNECTED=$(bluetoothctl devices Connected 2>/dev/null) + if [ -n \"$CONNECTED\" ]; then + echo \"connected=true\" + FIRST_DEV=$(echo \"$CONNECTED\" | head -1 | sed 's/Device [^ ]* //') + echo \"connectedDeviceName=$FIRST_DEV\" + else + echo \"connected=false\" + echo \"connectedDeviceName=\" + fi + + # Get paired devices + echo \"devices_start\" + bluetoothctl devices Paired 2>/dev/null | while read -r line; do + MAC=$(echo \"$line\" | awk '{print $2}') + NAME=$(echo \"$line\" | sed 's/Device [^ ]* //') + if bluetoothctl info \"$MAC\" 2>/dev/null | grep -q \"Connected: yes\"; then + echo \"device|$MAC|$NAME|connected\" + else + echo \"device|$MAC|$NAME|paired\" + fi + done + echo \"devices_end\" + + # Get discovered but not paired devices + echo \"available_start\" + PAIRED=$(bluetoothctl devices Paired 2>/dev/null | awk '{print $2}') + bluetoothctl devices 2>/dev/null | while read -r line; do + MAC=$(echo \"$line\" | awk '{print $2}') + NAME=$(echo \"$line\" | sed 's/Device [^ ]* //') + # Skip if already paired + if ! echo \"$PAIRED\" | grep -q \"$MAC\"; then + echo \"available|$MAC|$NAME\" + fi + done + echo \"available_end\" + " + + // Accumulated output from the process + property string btOutput: "" + property var pendingDevices: [] + property var pendingAvailable: [] + property bool parsingDevices: false + property bool parsingAvailableDevices: false + + // Run initial query on startup + Component.onCompleted: { + btProcess.running = true + } + + Process { + id: btProcess + command: ["sh", "-c", root.btQueryScript] + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => root.parseLine(data) + } + + onRunningChanged: { + if (running) { + // Reset state when process starts + root.pendingDevices = [] + root.pendingAvailable = [] + root.parsingDevices = false + root.parsingAvailableDevices = false + } + } + + onExited: { + // Finalize device lists + root.devices = root.pendingDevices + root.availableDevices = root.pendingAvailable + } + } + + function parseLine(line) { + if (!line || line.trim() === "") return + + if (line === "devices_start") { + parsingDevices = true + parsingAvailableDevices = false + return + } + if (line === "devices_end") { + parsingDevices = false + return + } + if (line === "available_start") { + parsingAvailableDevices = true + parsingDevices = false + return + } + if (line === "available_end") { + parsingAvailableDevices = false + return + } + + if (parsingDevices && line.startsWith("device|")) { + const parts = line.split("|") + if (parts.length >= 4) { + pendingDevices.push({ + mac: parts[1], + name: parts[2], + status: parts[3] + }) + } + return + } + + if (parsingAvailableDevices && line.startsWith("available|")) { + const parts = line.split("|") + if (parts.length >= 3) { + pendingAvailable.push({ + mac: parts[1], + name: parts[2] + }) + } + return + } + + const idx = line.indexOf("=") + if (idx === -1) return + + const key = line.substring(0, idx).trim() + const value = line.substring(idx + 1).trim() + + switch (key) { + case "available": + available = value === "true" + break + case "powered": + powered = value === "true" + break + case "discovering": + discovering = value === "true" + break + case "connected": + connected = value === "true" + break + case "connectedDeviceName": + connectedDeviceName = value + break + } + } + + // Update timer - faster when discovering + Timer { + interval: root.discovering ? 2000 : 10000 + running: true + repeat: true + onTriggered: btProcess.running = true + } + + // Control processes + Process { + id: powerProcess + command: ["bluetoothctl", "power", "on"] + onExited: btProcess.running = true + } + + // Scan process - runs continuously while discovering + Process { + id: scanProcess + command: ["bluetoothctl", "--timeout", "30", "scan", "on"] + + onExited: { + // Scan finished (timeout or stopped) + if (root.discovering) { + root.discovering = false + btProcess.running = true + } + } + } + + Process { + id: connectProcess + command: ["bluetoothctl", "connect", ""] + onExited: btProcess.running = true + } + + Process { + id: disconnectProcess + command: ["bluetoothctl", "disconnect", ""] + onExited: btProcess.running = true + } + + Process { + id: forgetProcess + command: ["bluetoothctl", "remove", ""] + onExited: btProcess.running = true + } + + Process { + id: pairProcess + command: ["bluetoothctl", "pair", ""] + onExited: btProcess.running = true + } + + // Control functions + function setPower(on) { + powerProcess.command = ["bluetoothctl", "power", on ? "on" : "off"] + powerProcess.running = true + powered = on // Optimistic update + } + + function startDiscovery() { + if (scanProcess.running) return // Already scanning + scanProcess.command = ["bluetoothctl", "--timeout", "30", "scan", "on"] + scanProcess.running = true + discovering = true // Optimistic update + } + + function stopDiscovery() { + if (scanProcess.running) { + scanProcess.running = false // Kill the scan process + } + discovering = false + btProcess.running = true // Refresh state + } + + function connectDevice(mac) { + connectProcess.command = ["bluetoothctl", "connect", mac] + connectProcess.running = true + } + + function disconnectDevice(mac) { + disconnectProcess.command = ["bluetoothctl", "disconnect", mac] + disconnectProcess.running = true + } + + function forgetDevice(mac) { + forgetProcess.command = ["bluetoothctl", "remove", mac] + forgetProcess.running = true + } + + function pairDevice(mac) { + // Enable pairable mode, then pair, then connect + pairProcess.command = ["sh", "-c", "bluetoothctl pairable on && bluetoothctl pair " + mac + " && bluetoothctl trust " + mac + " && bluetoothctl connect " + mac] + pairProcess.running = true + } + + function clearAvailableDevices() { + availableDevices = [] + } + + function refresh() { + btProcess.running = true + } +} diff --git a/dot_files/quickshell/services/Brightness.qml b/dot_files/quickshell/services/Brightness.qml new file mode 100644 index 0000000..9d85856 --- /dev/null +++ b/dot_files/quickshell/services/Brightness.qml @@ -0,0 +1,155 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Screen brightness control via brightnessctl (laptop) or ddcutil (external monitors) +Singleton { + id: root + + property real brightness: 1.0 + property int maxBrightness: 100 + property int currentBrightness: 100 + property string device: "" + property bool available: false + property string controlMethod: "none" // "backlight", "ddc", "none" + + Component.onCompleted: { + detectMethod.running = true + } + + // Detect which control method is available + Process { + id: detectMethod + command: ["sh", "-c", "if brightnessctl -l 2>/dev/null | grep -q backlight; then echo backlight; elif command -v ddcutil &>/dev/null && ddcutil detect --brief 2>/dev/null | grep -q Display; then echo ddc; else echo none; fi"] + running: false + onExited: { + if (!stdout) return + const method = stdout.trim() + controlMethod = method + available = (method !== "none") + if (method === "backlight") { + brightnessProcess.running = true + } else if (method === "ddc") { + ddcProcess.running = true + } + } + } + + // brightnessctl for laptop backlights + Process { + id: brightnessProcess + command: ["brightnessctl", "-m"] + running: false + onExited: parseBacklightOutput() + } + + function parseBacklightOutput() { + if (!brightnessProcess.stdout) return + const output = brightnessProcess.stdout.trim() + if (!output) return + + // brightnessctl -m format: device,class,current,percentage,max + const parts = output.split(",") + if (parts.length >= 5) { + device = parts[0] + currentBrightness = parseInt(parts[2]) || 100 + maxBrightness = parseInt(parts[4]) || 100 + brightness = currentBrightness / maxBrightness + } + } + + // ddcutil for external monitors + Process { + id: ddcProcess + command: ["ddcutil", "getvcp", "10", "--brief"] + running: false + onExited: parseDdcOutput() + } + + function parseDdcOutput() { + if (!ddcProcess.stdout) return + const output = ddcProcess.stdout.trim() + if (!output) return + + // ddcutil --brief format: VCP 10 C 75 100 + // VCP code, type, current, max + const parts = output.split(/\s+/) + if (parts.length >= 5) { + currentBrightness = parseInt(parts[3]) || 100 + maxBrightness = parseInt(parts[4]) || 100 + brightness = currentBrightness / maxBrightness + } + } + + // Update timer + Timer { + interval: 5000 // Longer interval for DDC (slower) + running: available + repeat: true + onTriggered: { + if (controlMethod === "backlight") { + brightnessProcess.running = true + } else if (controlMethod === "ddc") { + ddcProcess.running = true + } + } + } + + // Brightness control process + Process { + id: brightnessSetProcess + command: ["true"] + onExited: { + if (controlMethod === "backlight") { + brightnessProcess.running = true + } else if (controlMethod === "ddc") { + ddcProcess.running = true + } + } + } + + // Control functions + function setBrightness(value) { + const percent = Math.round(value * 100) + if (controlMethod === "backlight") { + brightnessSetProcess.command = ["brightnessctl", "set", percent + "%"] + } else if (controlMethod === "ddc") { + brightnessSetProcess.command = ["ddcutil", "setvcp", "10", String(percent)] + } else { + return + } + brightnessSetProcess.running = true + brightness = value + } + + function increase(step) { + if (controlMethod === "backlight") { + brightnessSetProcess.command = ["brightnessctl", "set", "+" + step + "%"] + } else if (controlMethod === "ddc") { + const newVal = Math.min(100, Math.round(brightness * 100) + step) + brightnessSetProcess.command = ["ddcutil", "setvcp", "10", String(newVal)] + } else { + return + } + brightnessSetProcess.running = true + } + + function decrease(step) { + if (controlMethod === "backlight") { + brightnessSetProcess.command = ["brightnessctl", "set", step + "%-"] + } else if (controlMethod === "ddc") { + const newVal = Math.max(0, Math.round(brightness * 100) - step) + brightnessSetProcess.command = ["ddcutil", "setvcp", "10", String(newVal)] + } else { + return + } + brightnessSetProcess.running = true + } + + // Brightness as percentage + function brightnessPercent() { + return Math.round(brightness * 100) + } +} diff --git a/dot_files/quickshell/services/DateTime.qml b/dot_files/quickshell/services/DateTime.qml new file mode 100644 index 0000000..1dafaac --- /dev/null +++ b/dot_files/quickshell/services/DateTime.qml @@ -0,0 +1,68 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + + property string timeString: "00:00" + property string dateString: "" + property string fullDateTime: "" + + property int hour: 0 + property int minute: 0 + property int second: 0 + property int day: 1 + property int month: 1 + property int year: 2024 + property int dayOfWeek: 0 + + readonly property var dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] + readonly property var monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + + function update() { + const now = new Date() + + hour = now.getHours() + minute = now.getMinutes() + second = now.getSeconds() + day = now.getDate() + month = now.getMonth() + 1 + year = now.getFullYear() + dayOfWeek = now.getDay() + + // Format time string (24-hour format) + timeString = pad(hour) + ":" + pad(minute) + + // Format date string + dateString = dayNames[dayOfWeek] + ", " + monthNames[month - 1] + " " + day + + // Full date time + fullDateTime = dateString + " " + year + " " + timeString + } + + function pad(num: int): string { + return num < 10 ? "0" + num : "" + num + } + + // Check if it's night time (for weather icons, etc.) + function isNight(): bool { + return hour < 6 || hour >= 20 + } + + // Get relative time string + function relativeTime(timestamp: var): string { + const now = new Date() + const then = new Date(timestamp) + const diff = Math.floor((now - then) / 1000) // seconds + + if (diff < 60) return "Just now" + if (diff < 3600) return Math.floor(diff / 60) + "m ago" + if (diff < 86400) return Math.floor(diff / 3600) + "h ago" + if (diff < 604800) return Math.floor(diff / 86400) + "d ago" + return then.toLocaleDateString() + } + + Component.onCompleted: update() +} diff --git a/dot_files/quickshell/services/IconResolver.qml b/dot_files/quickshell/services/IconResolver.qml new file mode 100644 index 0000000..976c163 --- /dev/null +++ b/dot_files/quickshell/services/IconResolver.qml @@ -0,0 +1,109 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Icon resolution service using datacube for flatpak and other icon lookups +Singleton { + id: root + + // Icon cache: maps query string to resolved icon path + property var iconCache: ({}) + // Track which queries are in progress + property var queriesInProgress: ({}) + + // Get icon for a given query (app name, window class, etc.) + // Returns cached path if available, empty string if not yet resolved + function getIcon(query) { + if (!query) return "" + + const cacheKey = query.toLowerCase() + + // Return from cache if available + if (iconCache[cacheKey]) { + return iconCache[cacheKey] + } + + // Trigger a lookup if not already in progress + if (!queriesInProgress[cacheKey]) { + lookupIcon(query, cacheKey) + } + + // Return empty - will update when cache populates + return "" + } + + // Force a lookup even if already cached (for refresh scenarios) + function refreshIcon(query) { + if (!query) return + const cacheKey = query.toLowerCase() + if (!queriesInProgress[cacheKey]) { + lookupIcon(query, cacheKey) + } + } + + // Check if an icon is cached + function hasIcon(query) { + if (!query) return false + return !!iconCache[query.toLowerCase()] + } + + // Internal: perform the datacube lookup + function lookupIcon(query, cacheKey) { + if (!query) return + + queriesInProgress[cacheKey] = true + + const proc = iconLookupComponent.createObject(root, { + query: query, + cacheKey: cacheKey + }) + proc.running = true + } + + Component { + id: iconLookupComponent + + Process { + id: iconProc + property string query: "" + property string cacheKey: "" + command: ["bash", "-lc", "datacube-cli query '" + query.replace(/'/g, "'\\''") + "' --json -m 1 -p applications"] + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => { + if (!data || data.trim() === "") return + try { + const item = JSON.parse(data) + if (item.icon) { + let iconPath + if (item.icon.startsWith("/")) { + iconPath = "file://" + item.icon + } else { + iconPath = "image://icon/" + item.icon + } + // Update the cache - create new object to trigger binding updates + let newCache = Object.assign({}, root.iconCache) + newCache[iconProc.cacheKey] = iconPath + root.iconCache = newCache + } + } catch (e) { + console.log("IconResolver: Failed to parse lookup result:", e) + } + } + } + + onExited: { + // Remove from in-progress tracking + let newInProgress = Object.assign({}, root.queriesInProgress) + delete newInProgress[iconProc.cacheKey] + root.queriesInProgress = newInProgress + + // Clean up this process object + iconProc.destroy() + } + } + } +} diff --git a/dot_files/quickshell/services/Network.qml b/dot_files/quickshell/services/Network.qml new file mode 100644 index 0000000..28e1f1a --- /dev/null +++ b/dot_files/quickshell/services/Network.qml @@ -0,0 +1,195 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property bool connected: false + property string type: "none" // wifi, ethernet, none + property string name: "" + property int strength: 0 // 0-100 for wifi + property string ipAddress: "" + + // Hardware availability + property bool wifiAvailable: false + property bool ethernetAvailable: false + + // Wifi specific + property string ssid: "" + property string bssid: "" + property int frequency: 0 + property string security: "" + + // VPN + property bool vpnActive: false + property string vpnName: "" + + // Network query script + readonly property string networkQueryScript: " + if command -v nmcli &>/dev/null; then + # Check hardware availability + if nmcli device 2>/dev/null | grep -q wifi; then + echo \"wifiAvailable=true\" + else + echo \"wifiAvailable=false\" + fi + if nmcli device 2>/dev/null | grep -q ethernet; then + echo \"ethernetAvailable=true\" + else + echo \"ethernetAvailable=false\" + fi + + CONN=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null | head -1) + if [ -n \"$CONN\" ]; then + NAME=$(echo \"$CONN\" | cut -d: -f1) + TYPE=$(echo \"$CONN\" | cut -d: -f2) + DEVICE=$(echo \"$CONN\" | cut -d: -f3) + + echo \"connected=true\" + echo \"name=$NAME\" + + case \"$TYPE\" in + *wireless*|*wifi*) + echo \"type=wifi\" + WIFI_INFO=$(nmcli -t -f SIGNAL,SSID,BSSID,FREQ,SECURITY device wifi list ifname \"$DEVICE\" 2>/dev/null | grep \"^\" | head -1) + if [ -n \"$WIFI_INFO\" ]; then + echo \"strength=$(echo $WIFI_INFO | cut -d: -f1)\" + echo \"ssid=$(echo $WIFI_INFO | cut -d: -f2)\" + echo \"frequency=$(echo $WIFI_INFO | cut -d: -f4 | tr -d ' MHz')\" + echo \"security=$(echo $WIFI_INFO | cut -d: -f5)\" + fi + ;; + *ethernet*) + echo \"type=ethernet\" + echo \"strength=100\" + ;; + *) + echo \"type=other\" + ;; + esac + + IP=$(nmcli -t -f IP4.ADDRESS device show \"$DEVICE\" 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1) + if [ -n \"$IP\" ]; then + echo \"ipAddress=$IP\" + fi + else + echo \"connected=false\" + echo \"type=none\" + fi + + VPN=$(nmcli -t -f NAME,TYPE connection show --active 2>/dev/null | grep vpn | head -1) + if [ -n \"$VPN\" ]; then + echo \"vpnActive=true\" + echo \"vpnName=$(echo $VPN | cut -d: -f1)\" + else + echo \"vpnActive=false\" + fi + else + # Check for wifi interfaces + if ls /sys/class/net/wl* 2>/dev/null | grep -q .; then + echo \"wifiAvailable=true\" + else + echo \"wifiAvailable=false\" + fi + # Check for ethernet interfaces + if ls /sys/class/net/e* 2>/dev/null | grep -q .; then + echo \"ethernetAvailable=true\" + else + echo \"ethernetAvailable=false\" + fi + + if ip route get 1.1.1.1 &>/dev/null; then + echo \"connected=true\" + IFACE=$(ip route get 1.1.1.1 | head -1 | awk '{print $5}') + echo \"name=$IFACE\" + if [[ \"$IFACE\" == wl* ]]; then + echo \"type=wifi\" + else + echo \"type=ethernet\" + fi + else + echo \"connected=false\" + echo \"type=none\" + fi + fi + " + + Process { + id: networkProcess + command: ["sh", "-c", root.networkQueryScript] + running: true + onExited: root.parseOutput() + } + + function parseOutput() { + const output = networkProcess.stdout + if (!output) return + const lines = output.split("\n") + + for (const line of lines) { + const idx = line.indexOf("=") + if (idx === -1) continue + + const key = line.substring(0, idx).trim() + const value = line.substring(idx + 1).trim() + + switch (key) { + case "connected": + connected = value === "true" + break + case "type": + type = value + break + case "name": + name = value + break + case "strength": + strength = parseInt(value) || 0 + break + case "ssid": + ssid = value + break + case "bssid": + bssid = value + break + case "frequency": + frequency = parseInt(value) || 0 + break + case "security": + security = value + break + case "ipAddress": + ipAddress = value + break + case "vpnActive": + vpnActive = value === "true" + break + case "vpnName": + vpnName = value + break + case "wifiAvailable": + wifiAvailable = value === "true" + break + case "ethernetAvailable": + ethernetAvailable = value === "true" + break + } + } + } + + // Update timer + Timer { + interval: 10000 // 10 seconds + running: true + repeat: true + onTriggered: networkProcess.running = true + } + + // Manually refresh + function refresh() { + networkProcess.running = true + } +} diff --git a/dot_files/quickshell/services/Notifications.qml b/dot_files/quickshell/services/Notifications.qml new file mode 100644 index 0000000..1c456ad --- /dev/null +++ b/dot_files/quickshell/services/Notifications.qml @@ -0,0 +1,93 @@ +pragma Singleton + +import QtQuick +import Quickshell + +// Notification manager service +// Works with the NotificationServer in shell.qml +Singleton { + id: root + + property var notifications: [] + property int unreadCount: 0 + + // Add a notification from the NotificationServer + function addNotification(notification) { + const hints = notification.hints || {} + + // Convert QML actions list to JS array with proper properties + let actionsArray = [] + if (notification.actions) { + for (let i = 0; i < notification.actions.length; i++) { + const action = notification.actions[i] + actionsArray.push({ + identifier: action.identifier || "", + text: action.text || "Action", + _raw: action // Keep reference for invoke() + }) + } + } + + const notif = { + id: notification.id || Date.now(), + appName: notification.appName || "Unknown", + appIcon: notification.appIcon || "", + summary: notification.summary || "", + body: notification.body || "", + image: notification.image || "", + actions: actionsArray, + hints: hints, + time: new Date(), + urgency: hints.urgency || 1, + persistent: hints.resident || false, + _raw: notification // Keep reference to original for dismiss/expire + } + + // Add to front + notifications = [notif, ...notifications] + unreadCount++ + + // Emit signal for popup + notificationAdded(notif) + } + + // Remove a notification + function removeNotification(id) { + notifications = notifications.filter(n => n.id !== id) + notificationRemoved(id) + } + + // Clear all notifications + function clearAll() { + notifications = [] + unreadCount = 0 + notificationsCleared() + } + + // Mark all as read + function markAllRead() { + unreadCount = 0 + } + + // Invoke an action on a notification + function invokeAction(id, actionId) { + const notif = notifications.find(n => n.id === id) + if (notif) { + // Find and invoke the raw action + const action = notif.actions.find(a => a.identifier === actionId) + if (action && action._raw && action._raw.invoke) { + action._raw.invoke() + } + actionInvoked(id, actionId) + if (!notif.persistent) { + removeNotification(id) + } + } + } + + // Signals + signal notificationAdded(var notification) + signal notificationRemoved(var id) + signal notificationsCleared() + signal actionInvoked(var id, string actionId) +} diff --git a/dot_files/quickshell/services/Privacy.qml b/dot_files/quickshell/services/Privacy.qml new file mode 100644 index 0000000..31a9277 --- /dev/null +++ b/dot_files/quickshell/services/Privacy.qml @@ -0,0 +1,145 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Monitors microphone and camera usage via PipeWire/PulseAudio +Singleton { + id: root + + property bool micInUse: false + property bool cameraInUse: false + property bool screenShareInUse: false + + // List of apps using mic/camera + property var micApps: [] + property var cameraApps: [] + + // Privacy query script + readonly property string privacyQueryScript: " + # Check microphone usage via PipeWire + if command -v pw-cli &>/dev/null; then + STREAMS=$(pw-cli list-objects 2>/dev/null | grep -B10 'media.class = \"Stream/Input/Audio\"' | grep 'application.name' | cut -d'\"' -f2 | sort -u | tr '\\n' ',' | sed 's/,$//') + if [ -n \"$STREAMS\" ]; then + echo \"micInUse=true\" + echo \"micApps=$STREAMS\" + else + echo \"micInUse=false\" + echo \"micApps=\" + fi + elif command -v pactl &>/dev/null; then + SOURCES=$(pactl list source-outputs 2>/dev/null | grep -c 'Source Output') + if [ \"$SOURCES\" -gt 0 ]; then + echo \"micInUse=true\" + APPS=$(pactl list source-outputs 2>/dev/null | grep 'application.name' | cut -d'\"' -f2 | tr '\\n' ',') + echo \"micApps=$APPS\" + else + echo \"micInUse=false\" + echo \"micApps=\" + fi + else + echo \"micInUse=false\" + echo \"micApps=\" + fi + + # Check camera usage via /dev/video* + CAM_PROCS=\"\" + for dev in /dev/video*; do + if [ -c \"$dev\" ]; then + PIDS=$(fuser \"$dev\" 2>/dev/null) + for pid in $PIDS; do + if [ -n \"$pid\" ] && [ -f \"/proc/$pid/comm\" ]; then + PROC=$(cat /proc/$pid/comm 2>/dev/null) + if [ -n \"$PROC\" ]; then + CAM_PROCS=\"$CAM_PROCS$PROC,\" + fi + fi + done + fi + done + CAM_PROCS=$(echo \"$CAM_PROCS\" | sed 's/,$//') + + if [ -n \"$CAM_PROCS\" ]; then + echo \"cameraInUse=true\" + echo \"cameraApps=$CAM_PROCS\" + else + echo \"cameraInUse=false\" + echo \"cameraApps=\" + fi + + # Check screen sharing + if command -v pw-cli &>/dev/null; then + SCREEN_STREAMS=$(pw-cli list-objects 2>/dev/null | grep -c 'media.class = \"Video/Source\"' || echo '0') + if [ \"$SCREEN_STREAMS\" -gt 0 ]; then + echo \"screenShareInUse=true\" + else + echo \"screenShareInUse=false\" + fi + else + echo \"screenShareInUse=false\" + fi + " + + Process { + id: privacyProcess + command: ["sh", "-c", root.privacyQueryScript] + running: true + + stdout: SplitParser { + splitMarker: "\n" + onRead: data => root.parseLine(data) + } + } + + function parseLine(line) { + if (!line || line.trim() === "") return + + const idx = line.indexOf("=") + if (idx === -1) return + + const key = line.substring(0, idx).trim() + const value = line.substring(idx + 1).trim() + + switch (key) { + case "micInUse": + micInUse = value === "true" + break + case "cameraInUse": + cameraInUse = value === "true" + break + case "screenShareInUse": + screenShareInUse = value === "true" + break + case "micApps": + micApps = value.split(",").filter(s => s.trim() !== "") + break + case "cameraApps": + cameraApps = value.split(" ").filter(s => s.trim() !== "") + break + } + } + + // Poll regularly + Timer { + interval: 2000 // 2 seconds + running: true + repeat: true + onTriggered: privacyProcess.running = true + } + + // Summary for tooltip + function summary() { + let parts = [] + if (micInUse) { + parts.push("Microphone: " + (micApps.length > 0 ? micApps.join(", ") : "in use")) + } + if (cameraInUse) { + parts.push("Camera: " + (cameraApps.length > 0 ? cameraApps.join(", ") : "in use")) + } + if (screenShareInUse) { + parts.push("Screen sharing active") + } + return parts.join("\n") || "No devices in use" + } +} diff --git a/dot_files/quickshell/services/SearchManager.qml b/dot_files/quickshell/services/SearchManager.qml new file mode 100644 index 0000000..d90db10 --- /dev/null +++ b/dot_files/quickshell/services/SearchManager.qml @@ -0,0 +1,166 @@ +pragma Singleton + +import QtQuick +import Quickshell + +import "providers" as Providers + +// Orchestrates search across all providers +Singleton { + id: root + + // Combined results from all active providers + property var results: [] + property bool searching: false + property string currentQuery: "" + + // Current search mode based on query prefix + property string currentMode: "search" // search, command, calc, file + + // Signals + signal resultsReady(var results) + signal searchStarted(string query) + signal searchComplete() + + // Search debounce timer to avoid excessive process spawning + Timer { + id: debounceTimer + interval: 50 + repeat: false + onTriggered: root.executeSearch() + } + + // Collect results timer + Timer { + id: collectTimer + interval: 150 // Wait for async providers + repeat: false + onTriggered: root.collectResults() + } + + function search(query) { + console.log("[SearchManager] search called with:", query) + currentQuery = query + + if (!query || query.trim() === "") { + results = [] + currentMode = "search" + searching = false + resultsReady([]) + return + } + + // Determine mode based on prefix + if (query.startsWith("/")) { + currentMode = "command" + } else if (query.startsWith("=")) { + currentMode = "calc" + } else if (query.startsWith(":")) { + currentMode = "file" + } else { + currentMode = "search" + } + + searching = true + searchStarted(query) + debounceTimer.restart() + } + + function executeSearch() { + const query = currentQuery + console.log("[SearchManager] executeSearch for:", query, "mode:", currentMode) + + // Route to appropriate provider(s) based on prefix + if (query.startsWith("=")) { + // Calculator + console.log("[SearchManager] routing to CalculatorProvider") + Providers.CalculatorProvider.search(query) + } else if (query.startsWith("/")) { + // Command + console.log("[SearchManager] routing to CommandProvider") + Providers.CommandProvider.search(query) + } else if (query.startsWith(":")) { + // File search + console.log("[SearchManager] routing to FileProvider") + Providers.FileProvider.search(query) + } else { + // Application search (default) + console.log("[SearchManager] routing to ApplicationProvider, loaded:", Providers.ApplicationProvider.loaded, "allApps count:", Providers.ApplicationProvider.allApps.length) + Providers.ApplicationProvider.search(query) + } + + // Collect results after a delay + collectTimer.restart() + } + + function collectResults() { + console.log("[SearchManager] collectResults called, mode:", currentMode) + let combinedResults = [] + + // Collect from the appropriate provider based on mode + if (currentMode === "calc") { + const calcResults = Providers.CalculatorProvider.results + console.log("[SearchManager] calc results:", calcResults ? calcResults.length : 0) + if (calcResults && calcResults.length > 0) { + for (let i = 0; i < calcResults.length; i++) { + combinedResults.push(calcResults[i]) + } + } + } else if (currentMode === "command") { + const cmdResults = Providers.CommandProvider.results + console.log("[SearchManager] command results:", cmdResults ? cmdResults.length : 0) + if (cmdResults && cmdResults.length > 0) { + for (let i = 0; i < cmdResults.length; i++) { + combinedResults.push(cmdResults[i]) + } + } + } else if (currentMode === "file") { + const fileResults = Providers.FileProvider.results + console.log("[SearchManager] file results:", fileResults ? fileResults.length : 0) + if (fileResults && fileResults.length > 0) { + for (let i = 0; i < fileResults.length; i++) { + combinedResults.push(fileResults[i]) + } + } + } else { + // Default: application search + const appResults = Providers.ApplicationProvider.results + console.log("[SearchManager] app results:", appResults ? appResults.length : 0) + if (appResults && appResults.length > 0) { + for (let i = 0; i < appResults.length; i++) { + combinedResults.push(appResults[i]) + } + } + } + + console.log("[SearchManager] total combined results:", combinedResults.length) + results = combinedResults + searching = false + resultsReady(combinedResults) + searchComplete() + } + + function clear() { + debounceTimer.stop() + collectTimer.stop() + currentQuery = "" + results = [] + currentMode = "search" + searching = false + + Providers.ApplicationProvider.clear() + Providers.CalculatorProvider.clear() + Providers.CommandProvider.clear() + Providers.FileProvider.clear() + } + + // Reload application provider + function reload() { + Providers.ApplicationProvider.reload() + } + + // Check if applications are loaded + function isReady() { + return Providers.ApplicationProvider.loaded + } +} diff --git a/dot_files/quickshell/services/Updates.qml b/dot_files/quickshell/services/Updates.qml new file mode 100644 index 0000000..a660f52 --- /dev/null +++ b/dot_files/quickshell/services/Updates.qml @@ -0,0 +1,116 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Package update checker +Singleton { + id: root + + property bool checking: false + property int updateCount: 0 + property var updates: [] + property string lastChecked: "" + property string error: "" + + // Update query script + readonly property string updateQueryScript: " + if command -v rpm-ostree &>/dev/null; then + STATUS=$(rpm-ostree status --json 2>/dev/null) + if echo \"$STATUS\" | grep -q '\"pending\"'; then + echo \"updateCount=1\" + echo \"update:System update available\" + else + UPDATES=$(rpm-ostree upgrade --check 2>/dev/null | grep -c \"Diff\" || echo \"0\") + echo \"updateCount=$UPDATES\" + fi + + elif command -v dnf &>/dev/null; then + UPDATES=$(dnf check-update --quiet 2>/dev/null | grep -c \"^[a-zA-Z]\" || echo \"0\") + echo \"updateCount=$UPDATES\" + + elif command -v apt &>/dev/null; then + apt update -qq 2>/dev/null + UPDATES=$(apt list --upgradable 2>/dev/null | grep -c \"upgradable\" || echo \"0\") + echo \"updateCount=$UPDATES\" + + elif command -v pacman &>/dev/null; then + UPDATES=$(checkupdates 2>/dev/null | wc -l || echo \"0\") + echo \"updateCount=$UPDATES\" + + else + echo \"updateCount=0\" + fi + + echo \"lastChecked=$(date '+%Y-%m-%d %H:%M')\" + " + + Process { + id: updateProcess + command: ["sh", "-c", root.updateQueryScript] + running: false + onExited: root.parseOutput() + onRunningChanged: { + if (running) checking = true + } + } + + function parseOutput() { + checking = false + error = "" + + const output = updateProcess.stdout + const lines = output.split("\n") + const newUpdates = [] + + for (const line of lines) { + if (line.startsWith("update:")) { + newUpdates.push(line.substring(7)) + continue + } + + const idx = line.indexOf("=") + if (idx === -1) continue + + const key = line.substring(0, idx).trim() + const value = line.substring(idx + 1).trim() + + switch (key) { + case "updateCount": + updateCount = parseInt(value) || 0 + break + case "lastChecked": + lastChecked = value + break + } + } + + updates = newUpdates + } + + // Check for updates + function check() { + if (!checking) { + updateProcess.running = true + } + } + + // Auto-check timer (every 6 hours) + Timer { + interval: 6 * 60 * 60 * 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: check() + } + + // Summary string + function summary(): string { + if (checking) return "Checking for updates..." + if (error) return "Error checking updates" + if (updateCount === 0) return "System is up to date" + if (updateCount === 1) return "1 update available" + return updateCount + " updates available" + } +} diff --git a/dot_files/quickshell/services/Weather.qml b/dot_files/quickshell/services/Weather.qml new file mode 100644 index 0000000..f99d178 --- /dev/null +++ b/dot_files/quickshell/services/Weather.qml @@ -0,0 +1,153 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +import "../modules/common" as Common + +Singleton { + id: root + + property bool ready: false + property bool loading: false + property string error: "" + + // Current weather + property string temperature: "" + property string feelsLike: "" + property string condition: "" + property string description: "" + property int humidity: 0 + property string windSpeed: "" + property string windDirection: "" + property string visibility: "" + property string pressure: "" + property string uvIndex: "" + + // Location + property string location: "" + property string region: "" + property string country: "" + + // Day/night for icons + property bool isNight: false + + // Icon based on condition + property string icon: Common.Icons.weatherIcon(condition, isNight) + + Process { + id: weatherProcess + command: ["curl", "-s", "--max-time", "10", + "wttr.in/" + (Common.Config.weatherLocation || "") + "?format=j1"] + + running: false + onExited: parseOutput() + } + + function parseOutput() { + loading = false + + const output = weatherProcess.stdout + if (!output || output.trim() === "") { + error = "No data received" + return + } + + try { + const data = JSON.parse(output) + + if (!data.current_condition || data.current_condition.length === 0) { + error = "Invalid weather data" + return + } + + const current = data.current_condition[0] + const area = data.nearest_area ? data.nearest_area[0] : null + + // Temperature + const units = Common.Config.weatherUnits + if (units === "imperial") { + temperature = current.temp_F + "°F" + feelsLike = current.FeelsLikeF + "°F" + windSpeed = current.windspeedMiles + " mph" + visibility = current.visibilityMiles + " mi" + } else { + temperature = current.temp_C + "°C" + feelsLike = current.FeelsLikeC + "°C" + windSpeed = current.windspeedKmph + " km/h" + visibility = current.visibility + " km" + } + + // Condition + condition = current.weatherDesc && current.weatherDesc[0] + ? current.weatherDesc[0].value + : "Unknown" + description = condition + + // Other data + humidity = parseInt(current.humidity) || 0 + windDirection = current.winddir16Point || "" + pressure = current.pressure + " mb" + uvIndex = current.uvIndex || "" + + // Location + if (area) { + location = area.areaName && area.areaName[0] + ? area.areaName[0].value + : "" + region = area.region && area.region[0] + ? area.region[0].value + : "" + country = area.country && area.country[0] + ? area.country[0].value + : "" + } + + // Check if it's night (simple check based on time) + const now = new Date() + const hour = now.getHours() + isNight = hour < 6 || hour >= 20 + + error = "" + ready = true + + } catch (e) { + error = "Failed to parse weather data: " + e.message + console.error("Weather parse error:", e) + } + } + + function refresh() { + if (loading) return + loading = true + weatherProcess.running = true + } + + // Auto-refresh timer + Timer { + interval: Common.Config.weatherUpdateInterval + running: true + repeat: true + triggeredOnStart: true + onTriggered: refresh() + } + + // Summary string for tooltip + function summary(): string { + if (!ready) return "Loading weather..." + if (error) return "Weather unavailable" + + let parts = [] + parts.push(condition) + parts.push("Feels like " + feelsLike) + parts.push("Humidity " + humidity + "%") + parts.push("Wind " + windSpeed + " " + windDirection) + + if (location) { + parts.unshift(location) + } + + return parts.join("\n") + } +} diff --git a/dot_files/quickshell/services/Windows.qml b/dot_files/quickshell/services/Windows.qml new file mode 100644 index 0000000..360a86d --- /dev/null +++ b/dot_files/quickshell/services/Windows.qml @@ -0,0 +1,94 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Window/client tracking service for app switcher +Singleton { + id: root + + property var windows: [] // List of windows sorted by focus history + property int currentIndex: 0 + property bool switcherActive: false + + // Query windows from Hyprland + readonly property string windowQueryScript: "hyprctl clients -j" + + Process { + id: windowProcess + command: ["sh", "-c", root.windowQueryScript] + + property string output: "" + + stdout: SplitParser { + splitMarker: "" + onRead: data => { + windowProcess.output += data + } + } + + onExited: { + try { + const clients = JSON.parse(windowProcess.output) + // Filter out hidden windows and sort by focusHistoryID + const visible = clients.filter(w => w.mapped && !w.hidden) + visible.sort((a, b) => a.focusHistoryID - b.focusHistoryID) + root.windows = visible + + // Pre-fetch icons for all window classes via IconResolver + for (const w of visible) { + if (w.class) { + IconResolver.getIcon(w.class) + } + } + } catch (e) { + console.log("Failed to parse windows:", e) + root.windows = [] + } + windowProcess.output = "" + } + } + + Process { + id: focusProcess + command: ["hyprctl", "dispatch", "focuswindow", "address:0x0"] + } + + function refresh() { + windowProcess.running = true + } + + function startSwitcher() { + refresh() + switcherActive = true + currentIndex = 0 + } + + function nextWindow() { + if (windows.length === 0) return + currentIndex = (currentIndex + 1) % windows.length + } + + function prevWindow() { + if (windows.length === 0) return + currentIndex = (currentIndex - 1 + windows.length) % windows.length + } + + function selectWindow() { + if (windows.length === 0 || currentIndex >= windows.length) { + switcherActive = false + return + } + + const window = windows[currentIndex] + focusProcess.command = ["hyprctl", "dispatch", "focuswindow", "address:" + window.address] + focusProcess.running = true + switcherActive = false + } + + function cancelSwitcher() { + switcherActive = false + currentIndex = 0 + } +} diff --git a/dot_files/quickshell/services/providers/ApplicationProvider.qml b/dot_files/quickshell/services/providers/ApplicationProvider.qml new file mode 100644 index 0000000..5fc6658 --- /dev/null +++ b/dot_files/quickshell/services/providers/ApplicationProvider.qml @@ -0,0 +1,259 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// Desktop application search provider +Singleton { + id: root + + // Provider metadata + readonly property string providerName: "Applications" + readonly property string prefix: "" // No prefix required - handles all queries + readonly property int priority: 10 + property bool enabled: true + + // State + property bool searching: false + property bool loaded: false + property var results: [] + property var allApps: [] + + // Configuration + property int maxResults: 50 + + // Signals + signal resultsReady(var results) + signal appsLoaded() + + // Fixed app loader script using ASCII delimiters to avoid conflicts + readonly property string appLoaderScript: ' + set -euo pipefail + + # ASCII delimiters that will not appear in desktop files + SEP=$(printf "\\x1f") # Unit Separator (ASCII 31) + REC=$(printf "\\x1e") # Record Separator (ASCII 30) + + parse_desktop() { + local file="$1" + local name="" generic="" icon="" exec_cmd="" comment="" nodisplay="false" categories="" + local in_desktop_entry=false + + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip empty lines and comments + [[ -z "$line" || "$line" == "#"* ]] && continue + + # Track section + if [[ "$line" == "["*"]" ]]; then + [[ "$line" == "[Desktop Entry]" ]] && in_desktop_entry=true || in_desktop_entry=false + continue + fi + + $in_desktop_entry || continue + + # Parse key=value (handle values with = in them) + local key="${line%%=*}" + local value="${line#*=}" + + # Skip localized entries (Name[en], etc.) + [[ "$key" == *"["*"]" ]] && continue + + case "$key" in + Name) [[ -z "$name" ]] && name="$value" ;; + GenericName) [[ -z "$generic" ]] && generic="$value" ;; + Icon) icon="$value" ;; + Exec) exec_cmd="$value" ;; + Comment) [[ -z "$comment" ]] && comment="$value" ;; + NoDisplay) nodisplay="$value" ;; + Categories) categories="$value" ;; + esac + done < "$file" + + # Skip NoDisplay=true or missing required fields + [[ "$nodisplay" == "true" ]] && return + [[ -z "$name" || -z "$exec_cmd" ]] && return + + # Clean exec: remove field codes (%f, %F, %u, %U, etc.) + exec_cmd=$(echo "$exec_cmd" | sed -E "s/ %[fFuUdDnNickvm]//g") + + # Output record with safe delimiters + printf "%s${SEP}%s${SEP}%s${SEP}%s${SEP}%s${SEP}%s${REC}" \ + "$name" "$generic" "$icon" "$exec_cmd" "$comment" "$categories" + } + + # Process system applications + if [[ -d "/usr/share/applications" ]]; then + for file in /usr/share/applications/*.desktop; do + [[ -f "$file" ]] && parse_desktop "$file" + done + fi + + # Process user applications (XDG compliant path) + user_apps="${XDG_DATA_HOME:-$HOME/.local/share}/applications" + if [[ -d "$user_apps" ]]; then + for file in "$user_apps"/*.desktop; do + [[ -f "$file" ]] && parse_desktop "$file" + done + fi + + # Process flatpak applications + flatpak_apps="/var/lib/flatpak/exports/share/applications" + if [[ -d "$flatpak_apps" ]]; then + for file in "$flatpak_apps"/*.desktop; do + [[ -f "$file" ]] && parse_desktop "$file" + done + fi + + # Process user flatpak applications + user_flatpak="${XDG_DATA_HOME:-$HOME/.local/share}/flatpak/exports/share/applications" + if [[ -d "$user_flatpak" ]]; then + for file in "$user_flatpak"/*.desktop; do + [[ -f "$file" ]] && parse_desktop "$file" + done + fi + ' + + Process { + id: appLoader + command: ["bash", "-c", root.appLoaderScript] + running: false + + onExited: { + root.searching = false + root.parseApps(this.stdout) + } + } + + function parseApps(output) { + console.log("[ApplicationProvider] parseApps called, output length:", output ? output.length : 0) + if (!output) { + console.log("[ApplicationProvider] no output, marking loaded") + loaded = true + appsLoaded() + return + } + + const recordSep = String.fromCharCode(30) // ASCII 30 - Record Separator + const unitSep = String.fromCharCode(31) // ASCII 31 - Unit Separator + + const records = output.split(recordSep).filter(r => r.trim() !== "") + const apps = [] + const seen = new Set() // Deduplicate by lowercase name + + for (const record of records) { + const parts = record.split(unitSep) + if (parts.length < 4) continue + + const name = parts[0] + const lowerName = name.toLowerCase() + + // Skip duplicates (prefer first occurrence) + if (seen.has(lowerName)) continue + seen.add(lowerName) + + apps.push({ + type: "app", + provider: "ApplicationProvider", + name: name, + genericName: parts[1] || "", + icon: getIconPath(parts[2]), + exec: parts[3], + description: parts[4] || parts[1] || "", + categories: parts[5] ? parts[5].split(";").filter(c => c) : [], + score: 1.0, + data: {} + }) + } + + // Sort alphabetically by name + apps.sort((a, b) => a.name.localeCompare(b.name)) + + console.log("[ApplicationProvider] parsed", apps.length, "apps") + allApps = apps + loaded = true + appsLoaded() + } + + function getIconPath(iconName) { + if (!iconName) return "" + if (iconName.startsWith("/")) return "file://" + iconName + return "image://icon/" + iconName + } + + function search(query) { + console.log("[ApplicationProvider] search called with:", query, "loaded:", loaded, "allApps:", allApps.length) + if (!loaded) { + // Apps not loaded yet - queue will be handled by appsLoaded + console.log("[ApplicationProvider] not loaded yet, returning empty") + results = [] + resultsReady([]) + return + } + + if (!query || query.trim() === "") { + results = [] + resultsReady([]) + return + } + + const lowerQuery = query.toLowerCase() + + // Score-based fuzzy matching + const scored = allApps + .map(app => { + let score = 0 + const lowerName = app.name.toLowerCase() + const lowerGeneric = (app.genericName || "").toLowerCase() + const lowerDesc = (app.description || "").toLowerCase() + + // Exact match at start = highest score + if (lowerName.startsWith(lowerQuery)) { + score = 1.0 - (lowerQuery.length / lowerName.length) * 0.1 + } + // Name contains query + else if (lowerName.includes(lowerQuery)) { + score = 0.7 + } + // Generic name match + else if (lowerGeneric.includes(lowerQuery)) { + score = 0.5 + } + // Description match + else if (lowerDesc.includes(lowerQuery)) { + score = 0.3 + } + + return Object.assign({}, app, { score: score }) + }) + .filter(app => app.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, maxResults) + + console.log("[ApplicationProvider] search found", scored.length, "results") + results = scored + resultsReady(scored) + } + + function clear() { + results = [] + } + + function canHandle(query) { + // Applications provider handles all non-prefixed queries + return query && !query.startsWith("/") && !query.startsWith("=") && !query.startsWith(":") + } + + function reload() { + console.log("[ApplicationProvider] reload() called") + loaded = false + allApps = [] + searching = true + appLoader.running = true + } + + Component.onCompleted: { + console.log("[ApplicationProvider] Component.onCompleted - starting reload") + reload() + } +} diff --git a/dot_files/quickshell/services/providers/CalculatorProvider.qml b/dot_files/quickshell/services/providers/CalculatorProvider.qml new file mode 100644 index 0000000..89e8193 --- /dev/null +++ b/dot_files/quickshell/services/providers/CalculatorProvider.qml @@ -0,0 +1,134 @@ +pragma Singleton + +import QtQuick +import Quickshell + +// Calculator/math expression provider +Singleton { + id: root + + // Provider metadata + readonly property string providerName: "Calculator" + readonly property string prefix: "=" + readonly property int priority: 5 // Highest priority when triggered + property bool enabled: true + + // State + property bool searching: false + property var results: [] + + // Signals + signal resultsReady(var results) + signal searchError(string error) + + function search(query) { + if (!query || !query.startsWith("=")) { + results = [] + resultsReady([]) + return + } + + const expr = query.substring(1).trim() + if (!expr) { + results = [] + resultsReady([]) + return + } + + try { + // Sanitize: only allow safe math characters and functions + // Allow: digits, operators, parentheses, decimal, common math functions + const sanitized = expr.replace(/\s+/g, "") + + // Check for invalid characters (basic safety check) + if (!/^[\d\s\+\-\*\/\(\)\.\,\^%a-zA-Z]+$/.test(sanitized)) { + throw new Error("Invalid characters in expression") + } + + // Replace common math notation + let jsExpr = sanitized + .replace(/\^/g, "**") // Exponentiation + .replace(/(\d)([a-zA-Z])/g, "$1*$2") // Implicit multiplication (2pi -> 2*pi) + + // Replace math constants + jsExpr = jsExpr + .replace(/\bpi\b/gi, "Math.PI") + .replace(/\be\b/g, "Math.E") + + // Replace math functions + jsExpr = jsExpr + .replace(/\bsqrt\(/gi, "Math.sqrt(") + .replace(/\babs\(/gi, "Math.abs(") + .replace(/\bsin\(/gi, "Math.sin(") + .replace(/\bcos\(/gi, "Math.cos(") + .replace(/\btan\(/gi, "Math.tan(") + .replace(/\blog\(/gi, "Math.log10(") + .replace(/\bln\(/gi, "Math.log(") + .replace(/\bexp\(/gi, "Math.exp(") + .replace(/\bpow\(/gi, "Math.pow(") + .replace(/\bfloor\(/gi, "Math.floor(") + .replace(/\bceil\(/gi, "Math.ceil(") + .replace(/\bround\(/gi, "Math.round(") + + // Safe evaluation using Function constructor + const result = Function('"use strict"; return (' + jsExpr + ')')() + + if (typeof result === "number" && !isNaN(result) && isFinite(result)) { + // Format result nicely + let formattedResult + if (Number.isInteger(result)) { + formattedResult = result.toString() + } else { + // Remove trailing zeros but keep precision + formattedResult = result.toPrecision(12).replace(/\.?0+$/, "") + } + + results = [{ + type: "calc", + provider: "CalculatorProvider", + name: formattedResult, + icon: "", + description: expr + " = " + formattedResult, + exec: "", // Will copy to clipboard on select + score: 1.0, + data: { + expression: expr, + result: result + } + }] + } else { + results = [{ + type: "calc", + provider: "CalculatorProvider", + name: "Invalid result", + icon: "", + description: "Expression did not evaluate to a valid number", + exec: "", + score: 0, + data: { error: true } + }] + } + } catch (e) { + results = [{ + type: "calc", + provider: "CalculatorProvider", + name: "Error", + icon: "", + description: e.message || "Invalid expression", + exec: "", + score: 0, + data: { error: true, message: e.message } + }] + } + + resultsReady(results) + } + + function clear() { + results = [] + } + + function canHandle(query) { + return query && query.startsWith("=") + } +} diff --git a/dot_files/quickshell/services/providers/CommandProvider.qml b/dot_files/quickshell/services/providers/CommandProvider.qml new file mode 100644 index 0000000..428cfd7 --- /dev/null +++ b/dot_files/quickshell/services/providers/CommandProvider.qml @@ -0,0 +1,62 @@ +pragma Singleton + +import QtQuick +import Quickshell + +// Shell command execution provider +Singleton { + id: root + + // Provider metadata + readonly property string providerName: "Commands" + readonly property string prefix: "/" + readonly property int priority: 15 + property bool enabled: true + + // State + property bool searching: false + property var results: [] + + // Signals + signal resultsReady(var results) + signal searchError(string error) + + function search(query) { + if (!query || !query.startsWith("/")) { + results = [] + resultsReady([]) + return + } + + const cmd = query.substring(1).trim() + if (!cmd) { + results = [] + resultsReady([]) + return + } + + results = [{ + type: "command", + provider: "CommandProvider", + name: cmd, + icon: "", + description: "Run shell command", + exec: cmd, + score: 1.0, + data: { + command: cmd, + terminal: false + } + }] + + resultsReady(results) + } + + function clear() { + results = [] + } + + function canHandle(query) { + return query && query.startsWith("/") + } +} diff --git a/dot_files/quickshell/services/providers/FileProvider.qml b/dot_files/quickshell/services/providers/FileProvider.qml new file mode 100644 index 0000000..be92a93 --- /dev/null +++ b/dot_files/quickshell/services/providers/FileProvider.qml @@ -0,0 +1,159 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +// File and directory search provider +Singleton { + id: root + + // Provider metadata + readonly property string providerName: "Files" + readonly property string prefix: ":" + readonly property int priority: 20 + property bool enabled: true + + // State + property bool searching: false + property var results: [] + property int maxResults: 20 + + // Signals + signal resultsReady(var results) + signal searchError(string error) + + // Current query for the process + property string currentQuery: "" + + // File search script using find + readonly property string fileSearchScript: ' + set -euo pipefail + QUERY="$1" + MAX="$2" + + SEP=$(printf "\\x1f") + REC=$(printf "\\x1e") + + # Expand ~ to home directory + QUERY="${QUERY/#\\~/$HOME}" + + # If exact path exists, show it first + if [[ -e "$QUERY" ]]; then + TYPE="file" + [[ -d "$QUERY" ]] && TYPE="directory" + NAME=$(basename "$QUERY") + printf "%s${SEP}%s${SEP}%s${REC}" "$NAME" "$QUERY" "$TYPE" + fi + + # Determine search directory and pattern + if [[ -d "$QUERY" ]]; then + # Query is a directory - list its contents + DIR="$QUERY" + PATTERN="*" + elif [[ "$QUERY" == */* ]]; then + # Query contains path separator + DIR=$(dirname "$QUERY" 2>/dev/null || echo "$HOME") + PATTERN=$(basename "$QUERY" 2>/dev/null || echo "*") + else + # Simple name - search from home + DIR="$HOME" + PATTERN="$QUERY" + fi + + # Ensure search directory exists + [[ -d "$DIR" ]] || DIR="$HOME" + + # Search for matching files (limit depth to avoid slowness) + find "$DIR" -maxdepth 3 -iname "*${PATTERN}*" -readable 2>/dev/null | head -n "$MAX" | while read -r path; do + # Skip the exact match we already output + [[ "$path" == "$QUERY" ]] && continue + + TYPE="file" + [[ -d "$path" ]] && TYPE="directory" + NAME=$(basename "$path") + printf "%s${SEP}%s${SEP}%s${REC}" "$NAME" "$path" "$TYPE" + done + ' + + Process { + id: fileSearchProcess + command: ["bash", "-c", root.fileSearchScript, "--", root.currentQuery, String(root.maxResults)] + running: false + + onExited: { + root.searching = false + root.parseResults(this.stdout) + } + } + + function parseResults(output) { + if (!output) { + results = [] + resultsReady([]) + return + } + + const recordSep = String.fromCharCode(30) + const unitSep = String.fromCharCode(31) + + const records = output.split(recordSep).filter(r => r.trim() !== "") + const files = [] + const seen = new Set() // Deduplicate + + for (const record of records) { + const parts = record.split(unitSep) + if (parts.length < 3) continue + + const path = parts[1] + if (seen.has(path)) continue + seen.add(path) + + const isDir = parts[2] === "directory" + files.push({ + type: "file", + provider: "FileProvider", + name: parts[0], + icon: isDir ? "image://icon/folder" : "image://icon/text-x-generic", + description: path, + exec: "xdg-open \"" + path.replace(/"/g, '\\"') + "\"", + score: 1.0, + data: { + path: path, + isDirectory: isDir + } + }) + } + + results = files + resultsReady(files) + } + + function search(query) { + if (!query || !query.startsWith(":")) { + results = [] + resultsReady([]) + return + } + + const path = query.substring(1).trim() + if (!path) { + results = [] + resultsReady([]) + return + } + + currentQuery = path + searching = true + fileSearchProcess.running = true + } + + function clear() { + results = [] + currentQuery = "" + } + + function canHandle(query) { + return query && query.startsWith(":") + } +} diff --git a/dot_files/quickshell/services/providers/qmldir b/dot_files/quickshell/services/providers/qmldir new file mode 100644 index 0000000..5415d53 --- /dev/null +++ b/dot_files/quickshell/services/providers/qmldir @@ -0,0 +1,6 @@ +module providers + +singleton ApplicationProvider 1.0 ApplicationProvider.qml +singleton CalculatorProvider 1.0 CalculatorProvider.qml +singleton FileProvider 1.0 FileProvider.qml +singleton CommandProvider 1.0 CommandProvider.qml diff --git a/dot_files/quickshell/services/qmldir b/dot_files/quickshell/services/qmldir new file mode 100644 index 0000000..55b6eae --- /dev/null +++ b/dot_files/quickshell/services/qmldir @@ -0,0 +1,15 @@ +module services + +singleton Audio 1.0 Audio.qml +singleton Battery 1.0 Battery.qml +singleton BluetoothStatus 1.0 BluetoothStatus.qml +singleton Brightness 1.0 Brightness.qml +singleton DateTime 1.0 DateTime.qml +singleton IconResolver 1.0 IconResolver.qml +singleton Network 1.0 Network.qml +singleton Notifications 1.0 Notifications.qml +singleton Privacy 1.0 Privacy.qml +singleton Updates 1.0 Updates.qml +singleton SearchManager 1.0 SearchManager.qml +singleton Weather 1.0 Weather.qml +singleton Windows 1.0 Windows.qml diff --git a/dot_files/quickshell/settings.qml b/dot_files/quickshell/settings.qml new file mode 100644 index 0000000..4641bab --- /dev/null +++ b/dot_files/quickshell/settings.qml @@ -0,0 +1,541 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +import "modules/common" as Common + +// Settings panel +PanelWindow { + id: root + + required property var screen + + anchors.centerIn: true + + width: 600 + height: 500 + color: "transparent" + + visible: GlobalStates.settingsOpen + + // Fade animation + opacity: GlobalStates.settingsOpen ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Common.Appearance.animation.standard + easing.type: Easing.OutCubic + } + } + + // Background + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.xlarge + color: Qt.rgba( + Common.Appearance.m3colors.surface.r, + Common.Appearance.m3colors.surface.g, + Common.Appearance.m3colors.surface.b, + Common.Appearance.overlayOpacity + ) + border.width: 1 + border.color: Common.Appearance.m3colors.outlineVariant + } + + RowLayout { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.large + spacing: Common.Appearance.spacing.large + + // Navigation sidebar + Rectangle { + Layout.preferredWidth: 160 + Layout.fillHeight: true + radius: Common.Appearance.rounding.large + color: Common.Appearance.m3colors.surfaceVariant + + ColumnLayout { + anchors.fill: parent + anchors.margins: Common.Appearance.spacing.small + spacing: 2 + + // Header + Text { + Layout.fillWidth: true + Layout.bottomMargin: Common.Appearance.spacing.small + text: "Settings" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.large + font.weight: Font.Medium + color: Common.Appearance.m3colors.onSurface + horizontalAlignment: Text.AlignHCenter + } + + Repeater { + model: [ + { id: "appearance", label: "Appearance", icon: Common.Icons.icons.settings }, + { id: "bar", label: "Status Bar", icon: Common.Icons.icons.menu }, + { id: "notifications", label: "Notifications", icon: Common.Icons.icons.notification }, + { id: "shortcuts", label: "Shortcuts", icon: Common.Icons.icons.code }, + { id: "about", label: "About", icon: Common.Icons.icons.info } + ] + + delegate: MouseArea { + Layout.fillWidth: true + Layout.preferredHeight: 40 + cursorShape: Qt.PointingHandCursor + onClicked: currentSection = modelData.id + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.medium + color: currentSection === modelData.id + ? Common.Appearance.m3colors.primaryContainer + : (parent.containsMouse + ? Common.Appearance.surfaceLayer(2) + : "transparent") + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Common.Appearance.spacing.small + anchors.rightMargin: Common.Appearance.spacing.small + spacing: Common.Appearance.spacing.small + + Text { + text: modelData.icon + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: currentSection === modelData.id + ? Common.Appearance.m3colors.onPrimaryContainer + : Common.Appearance.m3colors.onSurface + } + + Text { + Layout.fillWidth: true + text: modelData.label + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: currentSection === modelData.id + ? Common.Appearance.m3colors.onPrimaryContainer + : Common.Appearance.m3colors.onSurface + } + } + } + } + + Item { Layout.fillHeight: true } + + // Close button + MouseArea { + Layout.fillWidth: true + Layout.preferredHeight: 40 + cursorShape: Qt.PointingHandCursor + onClicked: GlobalStates.settingsOpen = false + + Rectangle { + anchors.fill: parent + radius: Common.Appearance.rounding.medium + color: parent.containsMouse + ? Common.Appearance.m3colors.errorContainer + : "transparent" + } + + RowLayout { + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Text { + text: Common.Icons.icons.close + font.family: Common.Appearance.fonts.icon + font.pixelSize: Common.Appearance.sizes.iconMedium + color: Common.Appearance.m3colors.error + } + + Text { + text: "Close" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.error + } + } + } + } + } + + // Content area + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + contentHeight: contentLoader.item ? contentLoader.item.implicitHeight : 0 + clip: true + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + Loader { + id: contentLoader + width: parent.width + sourceComponent: { + switch (currentSection) { + case "appearance": return appearanceSection + case "bar": return barSection + case "notifications": return notificationsSection + case "shortcuts": return shortcutsSection + case "about": return aboutSection + default: return appearanceSection + } + } + } + } + } + + property string currentSection: "appearance" + + // Appearance section + Component { + id: appearanceSection + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + SectionHeader { text: "Theme" } + + SettingRow { + label: "Dark mode" + description: "Use dark theme colors" + + Switch { + checked: Common.Config.darkMode + onCheckedChanged: { + Common.Config.darkMode = checked + Common.Config.save() + } + } + } + + SettingRow { + label: "Panel opacity" + description: "Transparency of panels and sidebars" + + RowLayout { + Slider { + Layout.preferredWidth: 150 + from: 0.5 + to: 1.0 + value: Common.Config.panelOpacity + onValueChanged: { + Common.Config.panelOpacity = value + Common.Config.save() + } + } + + Text { + text: Math.round(Common.Config.panelOpacity * 100) + "%" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + SectionHeader { text: "Fonts" } + + SettingRow { + label: "Font family" + description: "Main UI font" + + Text { + text: Common.Config.fontFamily + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + SettingRow { + label: "Font size" + description: "Base font size" + + RowLayout { + Slider { + Layout.preferredWidth: 100 + from: 10 + to: 18 + stepSize: 1 + value: Common.Config.fontSize + onValueChanged: { + Common.Config.fontSize = value + Common.Config.save() + } + } + + Text { + text: Common.Config.fontSize + "px" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + } + } + + // Bar section + Component { + id: barSection + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + SectionHeader { text: "Status Bar Items" } + + SettingRow { + label: "Show weather" + Switch { + checked: Common.Config.showWeather + onCheckedChanged: Common.Config.setValue("bar.showWeather", checked) + } + } + + SettingRow { + label: "Show battery" + Switch { + checked: Common.Config.showBattery + onCheckedChanged: Common.Config.setValue("bar.showBattery", checked) + } + } + + SettingRow { + label: "Show network" + Switch { + checked: Common.Config.showNetwork + onCheckedChanged: Common.Config.setValue("bar.showNetwork", checked) + } + } + + SettingRow { + label: "Show system tray" + Switch { + checked: Common.Config.showTray + onCheckedChanged: Common.Config.setValue("bar.showTray", checked) + } + } + + SettingRow { + label: "Show clock" + Switch { + checked: Common.Config.showClock + onCheckedChanged: Common.Config.setValue("bar.showClock", checked) + } + } + } + } + + // Notifications section + Component { + id: notificationsSection + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + SectionHeader { text: "Notification Behavior" } + + SettingRow { + label: "Notification timeout" + description: "How long notifications stay visible" + + RowLayout { + Slider { + Layout.preferredWidth: 150 + from: 2000 + to: 10000 + stepSize: 1000 + value: Common.Config.notificationTimeout + onValueChanged: Common.Config.setValue("notifications.timeout", value) + } + + Text { + text: (Common.Config.notificationTimeout / 1000) + "s" + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + SettingRow { + label: "Notification sounds" + Switch { + checked: Common.Config.notificationSounds + onCheckedChanged: Common.Config.setValue("notifications.sounds", checked) + } + } + } + } + + // Shortcuts section + Component { + id: shortcutsSection + + ColumnLayout { + spacing: Common.Appearance.spacing.medium + + SectionHeader { text: "Keyboard Shortcuts" } + + Repeater { + model: [ + { key: "Super + Space", action: "Open launcher" }, + { key: "Super + N", action: "Toggle notifications sidebar" }, + { key: "Super + A", action: "Toggle app launcher sidebar" }, + { key: "Super + Escape", action: "Close all panels" }, + { key: "Volume Keys", action: "Adjust volume" }, + { key: "Brightness Keys", action: "Adjust brightness" } + ] + + delegate: RowLayout { + Layout.fillWidth: true + spacing: Common.Appearance.spacing.medium + + Rectangle { + Layout.preferredWidth: 140 + Layout.preferredHeight: 28 + radius: Common.Appearance.rounding.small + color: Common.Appearance.m3colors.surfaceVariant + + Text { + anchors.centerIn: parent + text: modelData.key + font.family: Common.Appearance.fonts.mono + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + Text { + Layout.fillWidth: true + text: modelData.action + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + } + } + } + } + } + + // About section + Component { + id: aboutSection + + ColumnLayout { + spacing: Common.Appearance.spacing.large + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 100 + + ColumnLayout { + anchors.centerIn: parent + spacing: Common.Appearance.spacing.small + + Text { + Layout.alignment: Qt.AlignHCenter + text: "Hypercube" + font.family: Common.Appearance.fonts.title + font.pixelSize: Common.Appearance.fontSize.display + font.weight: Font.Medium + color: Common.Appearance.m3colors.primary + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: "Quickshell Configuration" + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + } + + SectionHeader { text: "System Information" } + + Repeater { + model: [ + { label: "Shell", value: "Quickshell" }, + { label: "Compositor", value: "Hyprland" }, + { label: "Theme", value: "Material Design 3 + Tokyonight" } + ] + + delegate: RowLayout { + Layout.fillWidth: true + + Text { + text: modelData.label + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurfaceVariant + } + + Item { Layout.fillWidth: true } + + Text { + text: modelData.value + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + } + } + } + } + } + + // Helper components + component SectionHeader: Text { + Layout.fillWidth: true + Layout.topMargin: Common.Appearance.spacing.small + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + font.weight: Font.Medium + color: Common.Appearance.m3colors.primary + textFormat: Text.PlainText + } + + component SettingRow: RowLayout { + property string label: "" + property string description: "" + default property alias content: contentItem.data + + Layout.fillWidth: true + spacing: Common.Appearance.spacing.medium + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + Text { + text: label + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.normal + color: Common.Appearance.m3colors.onSurface + } + + Text { + visible: description !== "" + text: description + font.family: Common.Appearance.fonts.main + font.pixelSize: Common.Appearance.fontSize.small + color: Common.Appearance.m3colors.onSurfaceVariant + } + } + + Item { + id: contentItem + Layout.preferredWidth: childrenRect.width + Layout.preferredHeight: childrenRect.height + } + } +} diff --git a/dot_files/quickshell/shell.qml b/dot_files/quickshell/shell.qml index a25aff2..1a80755 100644 --- a/dot_files/quickshell/shell.qml +++ b/dot_files/quickshell/shell.qml @@ -1,103 +1,189 @@ -//@ pragma UseQApplication +pragma ComponentBehavior: Bound + import QtQuick +import QtQuick.Controls import Quickshell import Quickshell.Io +import Quickshell.Wayland import Quickshell.Services.Notifications +import "modules/common" as Common +import "modules/bar" as Bar +import "modules/osd" as Osd +import "modules/sidebars" as Sidebars +import "modules/notifications" as NotificationsModule +import "modules/switcher" as Switcher +import "services" as Services + ShellRoot { id: root - // Global HUD visibility state - property bool hudVisible: false - - // Notification storage - property var notificationList: [] + Component.onCompleted: { + console.log("Hypercube Shell starting...") + Common.Config.load() + } // Notification server NotificationServer { id: notificationServer - bodySupported: true + bodyMarkupSupported: true actionsSupported: true imageSupported: true + persistenceSupported: true onNotification: (notification) => { - // Must set tracked = true to retain the notification object - notification.tracked = true; - root.addNotification(notification); + console.log("Notification received:", notification.summary) + // Add to notification service and update count + Services.Notifications.addNotification(notification) + GlobalStates.unreadNotificationCount = Services.Notifications.unreadCount } } - function toggleHud() { - hudVisible = !hudVisible; - } + // IPC Handler for external commands + // Use: qs ipc call shell [args] + // Example: qs ipc call shell toggleSidebarLeft + IpcHandler { + target: "shell" - function addNotification(notification) { - notificationList = [notification].concat(notificationList); - } + function toggleSidebarLeft() { + // Left sidebar always opens on leftmost screen + GlobalStates.activeScreen = GlobalStates.leftmostScreen || Quickshell.screens[0] + GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen + } - function dismissNotification(notification) { - if (notification) { - // Setting tracked = false dismisses and destroys the notification - notification.tracked = false; + function toggleSidebarRight() { + // Right sidebar always opens on rightmost screen + GlobalStates.activeScreen = GlobalStates.rightmostScreen || Quickshell.screens[0] + GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen } - notificationList = notificationList.filter(n => n !== notification); - } - function clearAllNotifications() { - for (var i = 0; i < notificationList.length; i++) { - var n = notificationList[i]; - if (n) { - n.tracked = false; + function showOsdVolume(value: real) { + if (GlobalStates.activeScreen === null) { + GlobalStates.activeScreen = Quickshell.screens[0] } + GlobalStates.osdType = "volume" + GlobalStates.osdValue = value + GlobalStates.osdVisible = true } - notificationList = []; - } - - // Handle IPC messages for HUD - IpcHandler { - target: "hud" - function toggle() { - root.toggleHud(); + function showOsdBrightness(value: real) { + if (GlobalStates.activeScreen === null) { + GlobalStates.activeScreen = Quickshell.screens[0] + } + GlobalStates.osdType = "brightness" + GlobalStates.osdValue = value + GlobalStates.osdVisible = true } - function show() { - root.hudVisible = true; + function showOsdMic(muted: bool) { + if (GlobalStates.activeScreen === null) { + GlobalStates.activeScreen = Quickshell.screens[0] + } + GlobalStates.osdType = "mic" + GlobalStates.osdMuted = muted + GlobalStates.osdVisible = true } - function hide() { - root.hudVisible = false; + function closeAll() { + GlobalStates.closeAll() } - } - // Keep launcher target for backwards compatibility - IpcHandler { - target: "launcher" + function startAppSwitcher() { + Services.Windows.startSwitcher() + } - function toggle() { - root.toggleHud(); + function nextWindow() { + if (Services.Windows.switcherActive) { + Services.Windows.nextWindow() + } else { + Services.Windows.startSwitcher() + } } - function show() { - root.hudVisible = true; + function prevWindow() { + if (Services.Windows.switcherActive) { + Services.Windows.prevWindow() + } else { + Services.Windows.startSwitcher() + } } - function hide() { - root.hudVisible = false; + function selectWindow() { + Services.Windows.selectWindow() } - } - // Command HUD - Hud { - showing: root.hudVisible - onClose: root.hudVisible = false - notificationList: root.notificationList - onDismissNotification: (notification) => root.dismissNotification(notification) - onClearAllNotifications: root.clearAllNotifications() + function cancelSwitcher() { + Services.Windows.cancelSwitcher() + } } - // OSD for volume/brightness - Osd { - id: osd + // Screen-specific components + Variants { + model: Quickshell.screens + + delegate: Component { + Item { + id: screenRoot + required property var modelData + property var screen: modelData + + // Top status bar (all screens) + Bar.StatusBar { + targetScreen: screenRoot.screen + } + + // OSD (on active screen, or primary if none set) + Loader { + active: GlobalStates.activeScreen === screenRoot.screen || + (GlobalStates.activeScreen === null && screenRoot.screen === Quickshell.screens[0]) + sourceComponent: Osd.Osd { + targetScreen: screenRoot.screen + } + } + + // Click catcher for sidebars (on ALL screens when any sidebar is open) + Loader { + active: GlobalStates.sidebarLeftOpen || GlobalStates.sidebarRightOpen + sourceComponent: Common.ClickCatcher { + targetScreen: screenRoot.screen + onClicked: GlobalStates.closeAll() + } + } + + // Left sidebar (only on leftmost screen) + Loader { + active: GlobalStates.isLeftmostScreen(screenRoot.screen) + sourceComponent: Sidebars.SidebarLeft { + targetScreen: screenRoot.screen + } + } + + // Right sidebar (only on rightmost screen) + Loader { + active: GlobalStates.isRightmostScreen(screenRoot.screen) + sourceComponent: Sidebars.SidebarRight { + targetScreen: screenRoot.screen + } + } + + // Notification popups (on active screen, or primary if none set) + Loader { + active: GlobalStates.activeScreen === screenRoot.screen || + (GlobalStates.activeScreen === null && screenRoot.screen === Quickshell.screens[0]) + sourceComponent: NotificationsModule.NotificationPopup { + targetScreen: screenRoot.screen + } + } + + // App switcher (on active screen only, or primary if none set) + Loader { + active: GlobalStates.activeScreen === screenRoot.screen || + (GlobalStates.activeScreen === null && screenRoot.screen === Quickshell.screens[0]) + sourceComponent: Switcher.AppSwitcher { + targetScreen: screenRoot.screen + } + } + } + } } } diff --git a/dot_files/quickshell/welcome-mode.qml b/dot_files/quickshell/welcome-mode.qml new file mode 100644 index 0000000..03c1758 --- /dev/null +++ b/dot_files/quickshell/welcome-mode.qml @@ -0,0 +1,692 @@ +// Standalone welcome mode for first-boot wizard +// This runs independently of the main shell for user creation + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import Quickshell.Io + +ShellRoot { + id: root + + // Welcome wizard runs on all screens, full screen + Variants { + model: Quickshell.screens + + delegate: Component { + PanelWindow { + id: welcomeWindow + required property var modelData + property var screen: modelData + + anchors.fill: true + color: "#1a1b26" // Tokyonight background + + // Only show wizard on primary screen + property bool isPrimary: screen === Quickshell.screens[0] + + // Background for non-primary screens + Rectangle { + anchors.fill: parent + visible: !isPrimary + color: "#1a1b26" + + Text { + anchors.centerIn: parent + text: "Hypercube" + font.pixelSize: 48 + font.weight: Font.Medium + color: "#7aa2f7" + opacity: 0.3 + } + } + + // Wizard on primary screen + Loader { + anchors.fill: parent + active: isPrimary + sourceComponent: WelcomeWizard {} + } + } + } + } + + // Welcome Wizard Component + component WelcomeWizard: Item { + anchors.fill: parent + + // Theme colors (Tokyonight) + readonly property color bgColor: "#1a1b26" + readonly property color surfaceColor: "#24283b" + readonly property color primaryColor: "#7aa2f7" + readonly property color textColor: "#c0caf5" + readonly property color subtextColor: "#a9b1d6" + readonly property color errorColor: "#f7768e" + + property int currentStep: 0 + property int totalSteps: 4 + + // User data + property string username: "" + property string fullName: "" + property string password: "" + property string passwordConfirm: "" + property string errorMessage: "" + + // Theme preferences + property bool selectedDarkMode: true + property string selectedAccent: "blue" + + ColumnLayout { + anchors.centerIn: parent + width: Math.min(parent.width - 100, 500) + spacing: 32 + + // Header + ColumnLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + Layout.alignment: Qt.AlignHCenter + text: "Welcome to Hypercube" + font.pixelSize: 36 + font.weight: Font.Medium + color: primaryColor + } + + Text { + Layout.alignment: Qt.AlignHCenter + text: getStepTitle() + font.pixelSize: 18 + color: subtextColor + } + } + + // Progress indicator + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 8 + + Repeater { + model: totalSteps + + Rectangle { + width: 40 + height: 4 + radius: 2 + color: index <= currentStep ? primaryColor : surfaceColor + } + } + } + + // Content area + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 320 + radius: 16 + color: surfaceColor + + Loader { + anchors.fill: parent + anchors.margins: 24 + sourceComponent: { + switch (currentStep) { + case 0: return usernameStep + case 1: return passwordStep + case 2: return themeStep + case 3: return confirmStep + default: return usernameStep + } + } + } + } + + // Error message + Text { + Layout.alignment: Qt.AlignHCenter + visible: errorMessage !== "" + text: errorMessage + font.pixelSize: 14 + color: errorColor + } + + // Navigation buttons + RowLayout { + Layout.fillWidth: true + spacing: 16 + + MouseArea { + visible: currentStep > 0 + Layout.preferredWidth: 100 + Layout.preferredHeight: 44 + cursorShape: Qt.PointingHandCursor + onClicked: { + errorMessage = "" + currentStep-- + } + + Rectangle { + anchors.fill: parent + radius: 12 + color: "transparent" + border.width: 1 + border.color: subtextColor + } + + Text { + anchors.centerIn: parent + text: "Back" + font.pixelSize: 14 + color: textColor + } + } + + Item { Layout.fillWidth: true } + + MouseArea { + Layout.preferredWidth: currentStep === totalSteps - 1 ? 160 : 100 + Layout.preferredHeight: 44 + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (validateStep()) { + if (currentStep === totalSteps - 1) { + createUser() + } else { + currentStep++ + } + } + } + + Rectangle { + anchors.fill: parent + radius: 12 + color: parent.containsMouse ? Qt.darker(primaryColor, 1.1) : primaryColor + } + + Text { + anchors.centerIn: parent + text: currentStep === totalSteps - 1 ? "Create Account" : "Next" + font.pixelSize: 14 + font.weight: Font.Medium + color: bgColor + } + } + } + } + + // Step 1: Username + Component { + id: usernameStep + + ColumnLayout { + spacing: 24 + + Text { + Layout.fillWidth: true + text: "Let's create your account. First, choose a username." + font.pixelSize: 14 + color: textColor + wrapMode: Text.WordWrap + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: "Username" + font.pixelSize: 12 + font.weight: Font.Medium + color: subtextColor + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: 8 + color: bgColor + border.width: usernameInput.activeFocus ? 2 : 1 + border.color: usernameInput.activeFocus ? primaryColor : subtextColor + + TextInput { + id: usernameInput + anchors.fill: parent + anchors.margins: 12 + font.pixelSize: 14 + color: textColor + clip: true + text: username + onTextChanged: username = text + Component.onCompleted: forceActiveFocus() + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: "Enter username" + font: usernameInput.font + color: subtextColor + visible: !usernameInput.text && !usernameInput.activeFocus + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: "Full Name (optional)" + font.pixelSize: 12 + font.weight: Font.Medium + color: subtextColor + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: 8 + color: bgColor + border.width: fullNameInput.activeFocus ? 2 : 1 + border.color: fullNameInput.activeFocus ? primaryColor : subtextColor + + TextInput { + id: fullNameInput + anchors.fill: parent + anchors.margins: 12 + font.pixelSize: 14 + color: textColor + clip: true + text: fullName + onTextChanged: fullName = text + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: "Enter your full name" + font: fullNameInput.font + color: subtextColor + visible: !fullNameInput.text && !fullNameInput.activeFocus + } + } + } + } + + Text { + Layout.fillWidth: true + text: "Username must be lowercase, start with a letter, and contain only letters, numbers, underscores, or hyphens." + font.pixelSize: 12 + color: subtextColor + wrapMode: Text.WordWrap + } + } + } + + // Step 2: Password + Component { + id: passwordStep + + ColumnLayout { + spacing: 24 + + Text { + Layout.fillWidth: true + text: "Create a secure password for your account." + font.pixelSize: 14 + color: textColor + wrapMode: Text.WordWrap + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: "Password" + font.pixelSize: 12 + font.weight: Font.Medium + color: subtextColor + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: 8 + color: bgColor + border.width: passwordInput.activeFocus ? 2 : 1 + border.color: passwordInput.activeFocus ? primaryColor : subtextColor + + TextInput { + id: passwordInput + anchors.fill: parent + anchors.margins: 12 + font.pixelSize: 14 + color: textColor + clip: true + echoMode: TextInput.Password + text: password + onTextChanged: password = text + Component.onCompleted: forceActiveFocus() + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: "Enter password" + font: passwordInput.font + color: subtextColor + visible: !passwordInput.text && !passwordInput.activeFocus + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + text: "Confirm Password" + font.pixelSize: 12 + font.weight: Font.Medium + color: subtextColor + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: 8 + color: bgColor + border.width: confirmInput.activeFocus ? 2 : 1 + border.color: confirmInput.activeFocus ? primaryColor : subtextColor + + TextInput { + id: confirmInput + anchors.fill: parent + anchors.margins: 12 + font.pixelSize: 14 + color: textColor + clip: true + echoMode: TextInput.Password + text: passwordConfirm + onTextChanged: passwordConfirm = text + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + text: "Confirm password" + font: confirmInput.font + color: subtextColor + visible: !confirmInput.text && !confirmInput.activeFocus + } + } + } + } + + Text { + Layout.fillWidth: true + text: "Password must be at least 8 characters." + font.pixelSize: 12 + color: subtextColor + wrapMode: Text.WordWrap + } + } + } + + // Step 3: Theme + Component { + id: themeStep + + ColumnLayout { + spacing: 24 + + Text { + Layout.fillWidth: true + text: "Choose your preferred appearance. You can change this later in Settings." + font.pixelSize: 14 + color: textColor + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + MouseArea { + Layout.fillWidth: true + Layout.preferredHeight: 80 + cursorShape: Qt.PointingHandCursor + onClicked: selectedDarkMode = true + + Rectangle { + anchors.fill: parent + radius: 12 + color: selectedDarkMode ? "#3d59a1" : bgColor + border.width: selectedDarkMode ? 2 : 1 + border.color: selectedDarkMode ? primaryColor : subtextColor + + Column { + anchors.centerIn: parent + spacing: 8 + + Text { text: "nights_stay"; font.family: "Material Symbols Rounded"; font.pixelSize: 32; color: textColor; anchors.horizontalCenter: parent.horizontalCenter } + Text { text: "Dark"; font.pixelSize: 14; color: textColor; anchors.horizontalCenter: parent.horizontalCenter } + } + } + } + + MouseArea { + Layout.fillWidth: true + Layout.preferredHeight: 80 + cursorShape: Qt.PointingHandCursor + onClicked: selectedDarkMode = false + + Rectangle { + anchors.fill: parent + radius: 12 + color: !selectedDarkMode ? "#3d59a1" : bgColor + border.width: !selectedDarkMode ? 2 : 1 + border.color: !selectedDarkMode ? primaryColor : subtextColor + + Column { + anchors.centerIn: parent + spacing: 8 + + Text { text: "sunny"; font.family: "Material Symbols Rounded"; font.pixelSize: 32; color: textColor; anchors.horizontalCenter: parent.horizontalCenter } + Text { text: "Light"; font.pixelSize: 14; color: textColor; anchors.horizontalCenter: parent.horizontalCenter } + } + } + } + } + + Text { + text: "Accent Color" + font.pixelSize: 12 + font.weight: Font.Medium + color: subtextColor + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Repeater { + model: [ + { id: "blue", color: "#7aa2f7" }, + { id: "green", color: "#9ece6a" }, + { id: "purple", color: "#bb9af7" }, + { id: "orange", color: "#ff9e64" }, + { id: "red", color: "#f7768e" }, + { id: "cyan", color: "#7dcfff" } + ] + + delegate: MouseArea { + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + cursorShape: Qt.PointingHandCursor + onClicked: selectedAccent = modelData.id + + Rectangle { + anchors.fill: parent + radius: 24 + color: modelData.color + + Rectangle { + anchors.centerIn: parent + width: 20 + height: 20 + radius: 10 + visible: selectedAccent === modelData.id + color: "white" + + Text { + anchors.centerIn: parent + text: "check" + font.family: "Material Symbols Rounded" + font.pixelSize: 14 + color: modelData.color + } + } + } + } + } + } + } + } + + // Step 4: Confirm + Component { + id: confirmStep + + ColumnLayout { + spacing: 24 + + Text { + Layout.fillWidth: true + text: "Review your settings and create your account." + font.pixelSize: 14 + color: textColor + wrapMode: Text.WordWrap + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: summaryCol.implicitHeight + 32 + radius: 12 + color: bgColor + + Column { + id: summaryCol + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + Row { + width: parent.width + Text { text: "Username"; font.pixelSize: 14; color: subtextColor; width: parent.width / 2 } + Text { text: username; font.pixelSize: 14; font.weight: Font.Medium; color: textColor } + } + Row { + width: parent.width + Text { text: "Full Name"; font.pixelSize: 14; color: subtextColor; width: parent.width / 2 } + Text { text: fullName || "(not set)"; font.pixelSize: 14; font.weight: Font.Medium; color: textColor } + } + Row { + width: parent.width + Text { text: "Theme"; font.pixelSize: 14; color: subtextColor; width: parent.width / 2 } + Text { text: selectedDarkMode ? "Dark" : "Light"; font.pixelSize: 14; font.weight: Font.Medium; color: textColor } + } + Row { + width: parent.width + Text { text: "Accent"; font.pixelSize: 14; color: subtextColor; width: parent.width / 2 } + Text { text: selectedAccent.charAt(0).toUpperCase() + selectedAccent.slice(1); font.pixelSize: 14; font.weight: Font.Medium; color: textColor } + } + } + } + + Text { + Layout.fillWidth: true + text: "Your account will be added to the 'wheel' group for administrator access." + font.pixelSize: 12 + color: subtextColor + wrapMode: Text.WordWrap + } + } + } + + function getStepTitle(): string { + switch (currentStep) { + case 0: return "Create Your Account" + case 1: return "Set Your Password" + case 2: return "Choose Your Theme" + case 3: return "Confirm Your Settings" + default: return "" + } + } + + function validateStep(): bool { + errorMessage = "" + + switch (currentStep) { + case 0: + if (!username || username.trim() === "") { + errorMessage = "Username is required" + return false + } + if (!/^[a-z_][a-z0-9_-]*$/.test(username)) { + errorMessage = "Invalid username format" + return false + } + if (username.length > 32) { + errorMessage = "Username too long (max 32 characters)" + return false + } + return true + + case 1: + if (!password || password.length < 8) { + errorMessage = "Password must be at least 8 characters" + return false + } + if (password !== passwordConfirm) { + errorMessage = "Passwords do not match" + return false + } + return true + + default: + return true + } + } + + function createUser() { + errorMessage = "" + userCreateProcess.running = true + } + + Process { + id: userCreateProcess + command: ["sh", "-c", + "useradd -m -G wheel -c '" + (fullName || username) + "' '" + username + "' && " + + "echo '" + username + ":" + password + "' | chpasswd && " + + // Save theme config for the new user + "mkdir -p /home/" + username + "/.config/hypercube && " + + "echo '{\"appearance\":{\"darkMode\":" + (selectedDarkMode ? "true" : "false") + ",\"accentColor\":\"" + selectedAccent + "\"}}' > /home/" + username + "/.config/hypercube/shell.json && " + + "chown -R " + username + ":" + username + " /home/" + username + "/.config" + ] + + running: false + onExited: { + if (exitCode === 0) { + // Exit quickshell, which will cause Hyprland to exit + Qt.quit() + } else { + errorMessage = "Failed to create user. Exit code: " + exitCode + } + } + } + } +} diff --git a/dot_files/wezterm/wezterm.lua b/dot_files/wezterm/wezterm.lua deleted file mode 100644 index 629a5ea..0000000 --- a/dot_files/wezterm/wezterm.lua +++ /dev/null @@ -1,241 +0,0 @@ -local wezterm = require("wezterm") -local config = wezterm.config_builder() -local act = wezterm.action - -config.color_scheme = "Tokyo Night" -config.font = wezterm.font("JetBrains Mono") -config.window_decorations = "NONE" -config.font_size = 13.0 -config.pane_focus_follows_mouse = false -config.tab_bar_at_bottom = true -config.automatically_reload_config = true -config.show_tab_index_in_tab_bar = false -config.tab_max_width = 32 - -config.use_fancy_tab_bar = false -config.show_new_tab_button_in_tab_bar = false -config.show_tab_index_in_tab_bar = false -config.hide_tab_bar_if_only_one_tab = true -config.max_fps = 120 - --- Pane border colors matching Tokyo Night theme -config.inactive_pane_hsb = { - saturation = 0.9, - brightness = 0.6, -} - --- The filled in variant of the < symbol -local SOLID_LEFT_ARROW = wezterm.nerdfonts.ple_ice_waveform_mirrored - --- The filled in variant of the > symbol -local SOLID_RIGHT_ARROW = wezterm.nerdfonts.ple_ice_waveform - --- This function returns the suggested title for a tab. --- It prefers the title that was set via `tab:set_title()` --- or `wezterm cli set-tab-title`, but falls back to the --- title of the active pane in that tab. -function tab_title(tab_info) - local title = tab_info.tab_title - -- if the tab title is explicitly set, take that - if title and #title > 0 then - return title - end - -- Otherwise, use the title from the active pane - -- in that tab - return tab_info.active_pane.title -end - -wezterm.on("format-tab-title", function(tab, tabs, panes, config, hover, max_width) - local edge_background = "#1a1b26" - local background = "#1a1b26" - local foreground = "#565f89" - - if tab.is_active then - background = "#24283b" - foreground = "#bb9af7" - elseif hover then - background = "#24283b" - end - - local edge_foreground = background - - local title = tab_title(tab) - - -- ensure that the titles fit in the available space, - -- and that we have room for the edges. - title = wezterm.truncate_right(title, max_width - 4) - - return { - { Background = { Color = edge_background } }, - { Foreground = { Color = edge_foreground } }, - { Text = SOLID_LEFT_ARROW }, - { Background = { Color = background } }, - { Foreground = { Color = foreground } }, - { Text = " " .. title .. " " }, - { Background = { Color = edge_background } }, - { Foreground = { Color = edge_foreground } }, - { Text = SOLID_RIGHT_ARROW }, - } -end) - -config.leader = { - key = "a", - mods = "CTRL", - timeout_milliseconds = 2000, -} - --- Custom key bindings -config.keys = { - { - key = "Enter", - mods = "ALT", - action = act.DisableDefaultAssignment, - }, - - -- Copy mode - { - key = "[", - mods = "LEADER", - action = act.ActivateCopyMode, - }, - - -- ---------------------------------------------------------------- - -- TABS - -- - -- Where possible, I'm using the same combinations as I would in tmux - -- ---------------------------------------------------------------- - - -- Show tab navigator; similar to listing panes in tmux - { - key = "w", - mods = "LEADER", - action = act.ShowTabNavigator, - }, - -- Create a tab (alternative to Ctrl-Shift-Tab) - { - key = "c", - mods = "LEADER", - action = act.SpawnTab("CurrentPaneDomain"), - }, - -- Rename current tab; analagous to command in tmux - { - key = ",", - mods = "LEADER", - action = act.PromptInputLine({ - description = "Enter new name for tab", - action = wezterm.action_callback(function(window, pane, line) - if line then - window:active_tab():set_title(line) - end - end), - }), - }, - -- Move to next/previous TAB - { - key = "n", - mods = "LEADER", - action = act.ActivateTabRelative(1), - }, - { - key = "p", - mods = "LEADER", - action = act.ActivateTabRelative(-1), - }, - -- Close tab - { - key = "&", - mods = "LEADER|SHIFT", - action = act.CloseCurrentTab({ confirm = true }), - }, - - -- ---------------------------------------------------------------- - -- PANES - -- - -- These are great and get me most of the way to replacing tmux - -- entirely, particularly as you can use "wezterm ssh" to ssh to another - -- server, and still retain Wezterm as your terminal there. - -- ---------------------------------------------------------------- - - -- -- Vertical split - { - -- | - key = "|", - mods = "LEADER|SHIFT", - action = act.SplitPane({ - direction = "Right", - size = { Percent = 50 }, - }), - }, - -- Horizontal split - { - -- - - key = "-", - mods = "LEADER", - action = act.SplitPane({ - direction = "Down", - size = { Percent = 50 }, - }), - }, - - -- Close/kill active pane - { - key = "x", - mods = "LEADER", - action = act.CloseCurrentPane({ confirm = true }), - }, - -- Swap active pane with another one - - { - key = "{", - mods = "LEADER|SHIFT", - action = act.PaneSelect({ mode = "SwapWithActiveKeepFocus" }), - }, - -- Zoom current pane (toggle) - { - key = "z", - mods = "LEADER", - action = act.TogglePaneZoomState, - }, - { - key = "f", - mods = "ALT", - action = act.TogglePaneZoomState, - }, - -- Move to next/previous pane - { - key = ";", - mods = "LEADER", - action = act.ActivatePaneDirection("Prev"), - }, - { - key = "o", - mods = "LEADER", - action = act.ActivatePaneDirection("Next"), - }, - - { key = "h", mods = "LEADER", action = act.ActivatePaneDirection("Left") }, - { key = "l", mods = "LEADER", action = act.ActivatePaneDirection("Right") }, - { key = "k", mods = "LEADER", action = act.ActivatePaneDirection("Up") }, - { key = "j", mods = "LEADER", action = act.ActivatePaneDirection("Down") }, - - { - key = "r", - mods = "LEADER", - action = act.ActivateKeyTable({ - name = "resize_pane", - one_shot = false, - }), - }, -} - -config.key_tables = { - resize_pane = { - { key = "h", action = act.AdjustPaneSize({ "Left", 1 }) }, - { key = "l", action = act.AdjustPaneSize({ "Right", 1 }) }, - { key = "k", action = act.AdjustPaneSize({ "Up", 1 }) }, - { key = "j", action = act.AdjustPaneSize({ "Down", 1 }) }, - { key = "Escape", action = "PopKeyTable" }, - }, -} - -return config diff --git a/flatpaks/system-flatpaks.list b/flatpaks/system-flatpaks.list new file mode 100644 index 0000000..5940626 --- /dev/null +++ b/flatpaks/system-flatpaks.list @@ -0,0 +1,2 @@ +# Hypercube System Flatpaks +be.alexandervanhee.gradia diff --git a/iso_files/anaconda/hypercube.conf b/iso_files/anaconda/hypercube.conf new file mode 100644 index 0000000..c9ff1e9 --- /dev/null +++ b/iso_files/anaconda/hypercube.conf @@ -0,0 +1,37 @@ +# Anaconda configuration file for Hypercube +# SPDX-License-Identifier: GPL-3.0-or-later + +[Profile] +# Hypercube is an ostree-based system, inherit from fedora-silverblue +profile_id = hypercube +base_profile = fedora-silverblue + +[Profile Detection] +# Match os-release values +os_id = hypercube + +[Network] +default_on_boot = FIRST_WIRED_WITH_LINK + +[Bootloader] +menu_auto_hide = True +# Use fedora EFI directory for compatibility +efi_dir = fedora + +[Storage] +# Hypercube uses BTRFS with compression +default_scheme = BTRFS +btrfs_compression = zstd:1 + +# Partitioning for ostree systems +default_partitioning = + / (min 1 GiB, max 70 GiB) + /home (min 500 MiB, free 50 GiB) + /var (btrfs) + +[User Interface] +custom_stylesheet = /usr/share/anaconda/pixmaps/hypercube/hypercube.css +# Hide user/password spokes - user creation happens on first boot via wizard +hidden_spokes = + PasswordSpoke + UserSpoke diff --git a/iso_files/anaconda/hypercube.css b/iso_files/anaconda/hypercube.css new file mode 100644 index 0000000..cfa4ed3 --- /dev/null +++ b/iso_files/anaconda/hypercube.css @@ -0,0 +1,149 @@ +/* Hypercube Anaconda Theme - Tokyo Night */ +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +/* Tokyo Night color palette */ +@define-color hypercube #7aa2f7; +@define-color tokyo_bg #1a1b26; +@define-color tokyo_bg_dark #16161e; +@define-color tokyo_fg #c0caf5; +@define-color tokyo_blue #7aa2f7; +@define-color tokyo_purple #bb9af7; +@define-color tokyo_dark_purple #414868; +@define-color tokyo_cyan #7dcfff; +@define-color tokyo_green #9ece6a; + +/* Sidebar background with Tokyo Night dark */ +.logo-sidebar { + background-image: none; + background-color: @tokyo_bg; + background-repeat: no-repeat; +} + +/* Hypercube logo in sidebar */ +.logo { + background-image: url('/usr/share/anaconda/pixmaps/hypercube/sidebar-logo.png'); + background-position: 50% 20px; + background-repeat: no-repeat; + background-color: transparent; +} + +/* Product logo placeholder */ +.product-logo { + background-image: none; + background-color: transparent; +} + +/* Navigation bar - Tokyo Night purple accent */ +AnacondaSpokeWindow #nav-box, +AnacondaHubWindow #nav-box { + background-color: @tokyo_purple; + background-image: none; + background-repeat: repeat; + color: white; +} + +/* Remove button shadow in nav-box */ +AnacondaSpokeWindow #nav-box GtkButton, +AnacondaHubWindow #nav-box GtkButton { + box-shadow: none; +} + +/* Main content area - dark background */ +AnacondaSpokeWindow, +AnacondaHubWindow { + background-color: @tokyo_bg_dark; +} + +/* Spoke buttons (main hub tiles) */ +.spoke-button { + background-color: @tokyo_dark_purple; + color: @tokyo_fg; + border-radius: 8px; +} + +.spoke-button:hover { + background-color: @tokyo_blue; + color: @tokyo_bg; +} + +.spoke-button:focus { + border-color: @tokyo_cyan; +} + +/* Selection highlights */ +*:selected { + background-color: @tokyo_blue; + color: @tokyo_bg; +} + +/* Links and accents */ +*:link { + color: @tokyo_cyan; +} + +/* Progress bar */ +progressbar progress { + background-color: @tokyo_purple; +} + +progressbar trough { + background-color: @tokyo_dark_purple; +} + +/* Entry fields */ +entry { + background-color: @tokyo_bg; + color: @tokyo_fg; + border-color: @tokyo_dark_purple; +} + +entry:focus { + border-color: @tokyo_blue; +} + +/* Buttons */ +button { + background-color: @tokyo_dark_purple; + color: @tokyo_fg; +} + +button:hover { + background-color: @tokyo_blue; + color: @tokyo_bg; +} + +button:active { + background-color: @tokyo_purple; +} + +/* Check buttons and radio buttons */ +checkbutton check, +radiobutton radio { + background-color: @tokyo_bg; + border-color: @tokyo_dark_purple; +} + +checkbutton check:checked, +radiobutton radio:checked { + background-color: @tokyo_blue; +} + +/* Lists and tree views */ +treeview { + background-color: @tokyo_bg; + color: @tokyo_fg; +} + +treeview:selected { + background-color: @tokyo_blue; + color: @tokyo_bg; +} + +/* Scrollbars */ +scrollbar slider { + background-color: @tokyo_dark_purple; +} + +scrollbar slider:hover { + background-color: @tokyo_blue; +} diff --git a/iso_files/anaconda/sidebar-logo.png b/iso_files/anaconda/sidebar-logo.png new file mode 100644 index 0000000..c3b4eb5 Binary files /dev/null and b/iso_files/anaconda/sidebar-logo.png differ diff --git a/iso_files/branding/background.png b/iso_files/branding/background.png new file mode 100644 index 0000000..53203f3 Binary files /dev/null and b/iso_files/branding/background.png differ diff --git a/iso_files/hide_hyprland_session.sh b/iso_files/hide_hyprland_session.sh deleted file mode 100755 index 59aa508..0000000 --- a/iso_files/hide_hyprland_session.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -# Hide Hyprland session so livesys detects GNOME for the live session -# After install, the full image with Hyprland session will be deployed -# This runs before rootfs-install-livesys-scripts -if [ -f /usr/share/wayland-sessions/hyprland.desktop ]; then - mv /usr/share/wayland-sessions/hyprland.desktop /usr/share/wayland-sessions/hyprland.desktop.hidden - echo "Hyprland session hidden - livesys will detect GNOME instead" -fi diff --git a/iso_files/hook-post-rootfs.sh b/iso_files/hook-post-rootfs.sh new file mode 100644 index 0000000..fd94ed7 --- /dev/null +++ b/iso_files/hook-post-rootfs.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# Hypercube Live ISO post-rootfs hook +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This script runs inside the rootfs before it gets squashed into the ISO. +# Files are available at /app/iso_files/ (copied from hypercube project) + +set -xeuo pipefail + +ISO_FILES="/app/iso_files" + +# Verify iso_files are available +if [[ ! -d "${ISO_FILES}" ]]; then + echo "ERROR: iso_files not found at ${ISO_FILES}" + echo "Make sure iso_files directory is copied to _titanoboa before building" + exit 1 +fi + +# Install Anaconda installer (Fedora's default installer) +dnf install -y anaconda anaconda-live + +# Install netcat for log sharing support +dnf install -y nmap-ncat + +# Configure Anaconda to use ostreecontainer instead of live image rsync +# This is required for bootc/ostree-based systems +# Get the embedded container image name from podman storage +CONTAINER_IMAGE=$(podman images --format '{{.Repository}}:{{.Tag}}' | head -1) +if [[ -n "${CONTAINER_IMAGE}" ]]; then + echo "Configuring Anaconda to install from container: ${CONTAINER_IMAGE}" + + # Add ostreecontainer command to interactive-defaults.ks + # This tells Anaconda to deploy the container image instead of rsync'ing the live filesystem + # The container image reference stored in podman will be whatever was used to build the ISO: + # - Local builds: your local registry (e.g., 10.0.7.113:5000/hypercube:stable-daily) + # - GitHub Actions: ghcr.io/binarypie-dev/hypercube:latest + # Using containers-storage transport means it uses the locally embedded image regardless of name + cat >> /usr/share/anaconda/interactive-defaults.ks << EOF +# Hypercube: Install from embedded container image using ostreecontainer +ostreecontainer --url=${CONTAINER_IMAGE} --transport=containers-storage --no-signature-verification --stateroot=hypercube +EOF +else + echo "WARNING: No container image found in podman storage" + echo "Installation may fail - ensure container is embedded during ISO build" +fi + +# Update BUG_REPORT_URL to point to Hypercube GitHub issues +sed -i 's|^BUG_REPORT_URL=.*|BUG_REPORT_URL="https://github.com/binarypie-dev/hypercube/issues"|' /usr/lib/os-release + +# Install Anaconda profile for Hypercube +mkdir -p /etc/anaconda/profile.d/ +install -m 0644 "${ISO_FILES}/anaconda/hypercube.conf" /etc/anaconda/profile.d/hypercube.conf + +# Create Anaconda pixmaps directory for Hypercube branding +mkdir -p /usr/share/anaconda/pixmaps/hypercube/ + +# Install Anaconda CSS theme (Tokyo Night) +install -m 0644 "${ISO_FILES}/anaconda/hypercube.css" /usr/share/anaconda/pixmaps/hypercube/hypercube.css + +# Install Hypercube sidebar logo for installer branding +install -m 0644 "${ISO_FILES}/anaconda/sidebar-logo.png" /usr/share/anaconda/pixmaps/hypercube/sidebar-logo.png + +# Copy Plymouth spinner frames for Anaconda loading animation (if available) +if [[ -d /usr/share/plymouth/themes/hypercube ]]; then + cp /usr/share/plymouth/themes/hypercube/animation-*.png /usr/share/anaconda/pixmaps/hypercube/ 2>/dev/null || true +fi + +# Copy base image Hyprland configs for live environment +# This gives users the full Hypercube experience to test before installing +if [[ -d /usr/share/hypercube/config/hypr ]]; then + mkdir -p /usr/share/hypr/config.live.d/ + cp /usr/share/hypercube/config/hypr/* /usr/share/hypr/config.live.d/ + + # Add live environment configuration + cat >> /usr/share/hypr/config.live.d/hyprland.conf << 'EOF' + +# Live ISO - Environment variables for XDG config discovery +# These are normally set by systemd user session but greetd skips that +env = XDG_CONFIG_DIRS,/etc/xdg:/usr/share/hypercube/config +env = XDG_DATA_DIRS,/usr/local/share:/usr/share:/usr/share/hypercube/data +env = GTK_THEME,Tokyonight-Dark +env = QT_QPA_PLATFORMTHEME,qt6ct + +# Live ISO - Window rules for Anaconda installer +# Prevent fullscreen, keep as tiled window so user can access terminal/nm-applet +windowrule = tile, class:^(anaconda)$ +windowrule = tile, class:^(liveinst)$ +windowrule = noinitialfocus, class:^(anaconda)$ +windowrule = noinitialfocus, class:^(liveinst)$ + +# Auto-launch terminal and network applet for live environment +exec-once = nm-applet --indicator +exec-once = $terminal + +# Live ISO - Auto-launch installer (after terminal so it doesn't grab focus first) +exec-once = sleep 1 && liveinst +EOF +fi + +# Configure livesys to use the hyprland session +# The livesys-hyprland script is already provided by livesys-scripts from binarypie/hypercube COPR +sed -i 's/^livesys_session=.*/livesys_session=hyprland/' /etc/sysconfig/livesys +# If the line doesn't exist, add it +grep -q '^livesys_session=' /etc/sysconfig/livesys || echo 'livesys_session=hyprland' >> /etc/sysconfig/livesys + +# Disable services that shouldn't run in live environment +# These are update/setup services that don't make sense on live media +systemctl disable rpm-ostree-countme.service || true +systemctl disable bootloader-update.service || true +systemctl disable rpm-ostreed-automatic.timer || true +systemctl disable flatpak-preinstall.service || true + +echo "Hypercube post-rootfs hook completed successfully" diff --git a/packages/README.md b/packages/README.md new file mode 100644 index 0000000..dfe20db --- /dev/null +++ b/packages/README.md @@ -0,0 +1,375 @@ +# Hypercube COPR Package Setup Guide + +This document lists all packages to configure in the `binarypie/hypercube` COPR repository. + +## COPR Repository Settings + +- **Project Name**: `hypercube` +- **Chroot Targets**: `fedora-43-x86_64` +- **Build Method**: SCM (Source Control Management) + +--- + +## Package Definitions + +For each package, use these settings in COPR: + +- **Type**: SCM +- **Clone URL**: `https://github.com/binarypie-dev/hypercube.git` +- **Committish**: `main` +- **Subdir**: (as listed below) +- **Spec File**: (as listed below) +- **SCM Type**: git + +--- + +## Priority 1: Hyprland Core Libraries + +These must be built first as other packages depend on them. + +### 1. hyprutils + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprutils` | +| Spec File | `hyprutils.spec` | +| Version | 0.11.0 | +| Dependencies | None (base library) | + +--- + +### 2. hyprlang + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprlang` | +| Spec File | `hyprlang.spec` | +| Version | 0.6.7 | +| Dependencies | hyprutils | + +--- + +### 3. hyprwayland-scanner + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprwayland-scanner` | +| Spec File | `hyprwayland-scanner.spec` | +| Version | 0.4.5 | +| Dependencies | None | + +--- + +### 4. hyprgraphics + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprgraphics` | +| Spec File | `hyprgraphics.spec` | +| Version | 0.4.0 | +| Dependencies | hyprutils | + +--- + +### 5. hyprcursor + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprcursor` | +| Spec File | `hyprcursor.spec` | +| Version | 0.1.13 | +| Dependencies | hyprlang | + +--- + +### 6. hyprland-protocols + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprland-protocols` | +| Spec File | `hyprland-protocols.spec` | +| Version | 0.7.0 | +| Dependencies | None | + +--- + +### 7. aquamarine + +| Setting | Value | +|---------|-------| +| Subdir | `packages/aquamarine` | +| Spec File | `aquamarine.spec` | +| Version | 0.10.0 | +| Dependencies | hyprutils, hyprwayland-scanner | + +--- + +### 8. hyprland-qt-support + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprland-qt-support` | +| Spec File | `hyprland-qt-support.spec` | +| Version | 0.1.0 | +| Dependencies | hyprlang | + +--- + +### 9. glaze + +| Setting | Value | +|---------|-------| +| Subdir | `packages/glaze` | +| Spec File | `glaze.spec` | +| Version | 6.1.0 | +| Dependencies | None (header-only library) | +| Notes | Provides glaze-static for hyprland | + +--- + +### 10. uwsm + +| Setting | Value | +|---------|-------| +| Subdir | `packages/uwsm` | +| Spec File | `uwsm.spec` | +| Version | 0.23.3 | +| Dependencies | None (Python-based, uses system packages) | +| Notes | Universal Wayland Session Manager, required by hyprland-uwsm subpackage | + +--- + +## Priority 2: Hyprland Compositor & Tools + +Build after core libraries are available. + +### 11. hyprland + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprland` | +| Spec File | `hyprland.spec` | +| Version | 0.52.2 | +| Dependencies | aquamarine, hyprcursor, hyprgraphics, hyprlang, hyprutils, hyprwayland-scanner | + +--- + +### 12. hyprlock + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprlock` | +| Spec File | `hyprlock.spec` | +| Version | 0.9.2 | +| Dependencies | hyprgraphics, hyprlang, hyprutils, hyprwayland-scanner | + +--- + +### 13. hypridle + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hypridle` | +| Spec File | `hypridle.spec` | +| Version | 0.1.7 | +| Dependencies | hyprland-protocols, hyprlang, hyprutils, hyprwayland-scanner | + +--- + +### 14. hyprpaper + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprpaper` | +| Spec File | `hyprpaper.spec` | +| Version | 0.7.6 | +| Dependencies | hyprgraphics, hyprlang, hyprutils, hyprwayland-scanner | + +--- + +### 15. xdg-desktop-portal-hyprland + +| Setting | Value | +|---------|-------| +| Subdir | `packages/xdg-desktop-portal-hyprland` | +| Spec File | `xdg-desktop-portal-hyprland.spec` | +| Version | 1.3.11 | +| Dependencies | hyprland-protocols, hyprlang, hyprutils, hyprwayland-scanner | + +--- + +### 16. hyprpolkitagent + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprpolkitagent` | +| Spec File | `hyprpolkitagent.spec` | +| Version | 0.1.3 | +| Dependencies | hyprutils, hyprland-qt-support | + +--- + +### 17. hyprtoolkit + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprtoolkit` | +| Spec File | `hyprtoolkit.spec` | +| Version | 0.4.1 | +| Dependencies | aquamarine, hyprgraphics, hyprlang, hyprutils, hyprwayland-scanner | +| Notes | Modern C++ Wayland-native GUI toolkit for Hyprland utilities | + +--- + +### 18. hyprland-guiutils + +| Setting | Value | +|---------|-------| +| Subdir | `packages/hyprland-guiutils` | +| Spec File | `hyprland-guiutils.spec` | +| Version | 0.2.0 | +| Dependencies | aquamarine, hyprtoolkit, hyprlang, hyprutils | +| Notes | Successor to hyprland-qtutils. Provides dialog, donate-screen, run, update-screen, welcome utilities | + +--- + +## Priority 3: CLI Tools + +These have no hyprland dependencies and can be built in parallel. + +### 19. eza + +| Setting | Value | +|---------|-------| +| Subdir | `packages/eza` | +| Spec File | `eza.spec` | +| Version | 0.20.21 | +| Dependencies | None | + +--- + +### 20. starship + +| Setting | Value | +|---------|-------| +| Subdir | `packages/starship` | +| Spec File | `starship.spec` | +| Version | 1.24.1 | +| Dependencies | None | + +--- + +### 21. lazygit + +| Setting | Value | +|---------|-------| +| Subdir | `packages/lazygit` | +| Spec File | `lazygit.spec` | +| Version | 0.57.0 | +| Dependencies | None | + +--- + +### 22. ghostty + +| Setting | Value | +|---------|-------| +| Subdir | `packages/ghostty` | +| Spec File | `ghostty.spec` | +| Version | 1.2.3^git (main branch) | +| Dependencies | None | +| Notes | Building from main branch for zig 0.15.2 compatibility | + +--- + +### 23. quickshell + +| Setting | Value | +|---------|-------| +| Subdir | `packages/quickshell` | +| Spec File | `quickshell.spec` | +| Version | 0.2.1 | +| Dependencies | None | + +--- + +### 24. livesys-scripts + +| Setting | Value | +|---------|-------| +| Subdir | `packages/livesys-scripts` | +| Spec File | `livesys-scripts.spec` | +| Version | 0.9.1 | +| Dependencies | None | +| Notes | Source is from Pagure fork, not GitHub | + +--- + +## Build Order Summary + +To ensure dependencies are satisfied, build in this order: + +**Batch 1** (no dependencies): +1. hyprutils +2. hyprwayland-scanner +3. hyprland-protocols +4. glaze +5. uwsm +6. eza +7. starship +8. lazygit +9. ghostty +10. quickshell +11. livesys-scripts + +**Batch 2** (depends on Batch 1): +1. hyprlang (needs hyprutils) +2. hyprgraphics (needs hyprutils) +3. aquamarine (needs hyprutils, hyprwayland-scanner) + +**Batch 3** (depends on Batch 2): +1. hyprcursor (needs hyprlang) +2. hyprland-qt-support (needs hyprlang) + +**Batch 4** (depends on Batch 3): +1. hyprland (needs aquamarine, hyprcursor, hyprgraphics, hyprlang, hyprutils, glaze) +2. hyprlock (needs hyprgraphics, hyprlang, hyprutils, hyprwayland-scanner) +3. hypridle (needs hyprland-protocols, hyprlang, hyprutils, hyprwayland-scanner) +4. hyprpaper (needs hyprgraphics, hyprlang, hyprutils, hyprwayland-scanner) +5. xdg-desktop-portal-hyprland (needs hyprland-protocols, hyprlang, hyprutils, hyprwayland-scanner) +6. hyprpolkitagent (needs hyprutils, hyprland-qt-support) +7. hyprtoolkit (needs aquamarine, hyprgraphics, hyprlang, hyprutils, hyprwayland-scanner) + +**Batch 5** (depends on Batch 4): +1. hyprland-guiutils (needs aquamarine, hyprtoolkit, hyprlang, hyprutils) + +--- + +## Quick Reference Table + +| # | Package | Subdir | Spec File | Version | +|---|---------|--------|-----------|---------| +| 1 | hyprutils | `packages/hyprutils` | `hyprutils.spec` | 0.11.0 | +| 2 | hyprlang | `packages/hyprlang` | `hyprlang.spec` | 0.6.7 | +| 3 | hyprwayland-scanner | `packages/hyprwayland-scanner` | `hyprwayland-scanner.spec` | 0.4.5 | +| 4 | hyprgraphics | `packages/hyprgraphics` | `hyprgraphics.spec` | 0.4.0 | +| 5 | hyprcursor | `packages/hyprcursor` | `hyprcursor.spec` | 0.1.13 | +| 6 | hyprland-protocols | `packages/hyprland-protocols` | `hyprland-protocols.spec` | 0.7.0 | +| 7 | aquamarine | `packages/aquamarine` | `aquamarine.spec` | 0.10.0 | +| 8 | hyprland-qt-support | `packages/hyprland-qt-support` | `hyprland-qt-support.spec` | 0.1.0 | +| 9 | glaze | `packages/glaze` | `glaze.spec` | 6.1.0 | +| 10 | uwsm | `packages/uwsm` | `uwsm.spec` | 0.23.3 | +| 11 | hyprland | `packages/hyprland` | `hyprland.spec` | 0.52.2 | +| 12 | hyprlock | `packages/hyprlock` | `hyprlock.spec` | 0.9.2 | +| 13 | hypridle | `packages/hypridle` | `hypridle.spec` | 0.1.7 | +| 14 | hyprpaper | `packages/hyprpaper` | `hyprpaper.spec` | 0.7.6 | +| 15 | xdg-desktop-portal-hyprland | `packages/xdg-desktop-portal-hyprland` | `xdg-desktop-portal-hyprland.spec` | 1.3.11 | +| 16 | hyprpolkitagent | `packages/hyprpolkitagent` | `hyprpolkitagent.spec` | 0.1.3 | +| 17 | hyprtoolkit | `packages/hyprtoolkit` | `hyprtoolkit.spec` | 0.4.1 | +| 18 | hyprland-guiutils | `packages/hyprland-guiutils` | `hyprland-guiutils.spec` | 0.2.0 | +| 19 | eza | `packages/eza` | `eza.spec` | 0.20.21 | +| 20 | starship | `packages/starship` | `starship.spec` | 1.24.1 | +| 21 | lazygit | `packages/lazygit` | `lazygit.spec` | 0.57.0 | +| 22 | ghostty | `packages/ghostty` | `ghostty.spec` | 1.2.3^git | +| 23 | quickshell | `packages/quickshell` | `quickshell.spec` | 0.2.1 | +| 24 | livesys-scripts | `packages/livesys-scripts` | `livesys-scripts.spec` | 0.9.1 | diff --git a/packages/aquamarine/aquamarine.spec b/packages/aquamarine/aquamarine.spec new file mode 100644 index 0000000..e32d876 --- /dev/null +++ b/packages/aquamarine/aquamarine.spec @@ -0,0 +1,61 @@ +Name: aquamarine +Version: 0.10.0 +Release: 1%{?dist} +Summary: A very light linux rendering backend library + +License: BSD-3-Clause +URL: https://github.com/hyprwm/aquamarine +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: mesa-libEGL-devel +BuildRequires: pkgconfig(gbm) +BuildRequires: pkgconfig(hwdata) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(hyprwayland-scanner) +BuildRequires: pkgconfig(libdisplay-info) +BuildRequires: pkgconfig(libdrm) +BuildRequires: pkgconfig(libinput) +BuildRequires: pkgconfig(libseat) +BuildRequires: pkgconfig(libudev) +BuildRequires: pkgconfig(pixman-1) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-protocols) + +%description +%{summary}. + +%package devel +Summary: Development files for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Development files for %{name}. + +%prep +%autosetup -p1 + +%build +%cmake -DCMAKE_BUILD_TYPE=Release +%cmake_build + +%install +%cmake_install + +%files +%license LICENSE +%doc README.md +%{_libdir}/lib%{name}.so.%{version} +%{_libdir}/lib%{name}.so.9 + +%files devel +%{_includedir}/%{name}/ +%{_libdir}/lib%{name}.so +%{_libdir}/pkgconfig/%{name}.pc + +%changelog +* Mon Dec 16 2024 Hypercube - 0.10.0-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/eza/eza.spec b/packages/eza/eza.spec new file mode 100644 index 0000000..76ef7ee --- /dev/null +++ b/packages/eza/eza.spec @@ -0,0 +1,59 @@ +# NOTE: This package requires "Enable internet access during builds" in COPR settings + +%global debug_package %{nil} + +Name: eza +Version: 0.20.21 +Release: 1%{?dist} +Summary: Modern replacement for ls + +License: EUPL-1.2 +URL: https://github.com/eza-community/eza +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +BuildRequires: cargo +BuildRequires: rust +BuildRequires: pandoc + +%description +eza is a modern replacement for the venerable file-listing command-line program +ls that ships with Unix and Linux operating systems, giving it more features +and better defaults. + +%prep +%autosetup -n %{name}-%{version} + +%build +export RUSTFLAGS="%{build_rustflags}" +cargo build --release --locked + +%install +install -Dpm 0755 target/release/%{name} -t %{buildroot}%{_bindir}/ + +# Generate and install man pages +mkdir -p target/man +for page in eza.1 eza_colors.5 eza_colors-explanation.5; do + sed "s/\$version/v%{version}/g" "man/${page}.md" | pandoc --standalone -f markdown -t man > "target/man/${page}" +done +install -Dpm 0644 target/man/eza.1 -t %{buildroot}/%{_mandir}/man1/ +install -Dpm 0644 target/man/eza_colors.5 -t %{buildroot}/%{_mandir}/man5/ +install -Dpm 0644 target/man/eza_colors-explanation.5 -t %{buildroot}/%{_mandir}/man5/ + +# Install shell completions +install -Dpm 0644 completions/bash/%{name} -t %{buildroot}/%{_datadir}/bash-completion/completions/ +install -Dpm 0644 completions/zsh/_%{name} -t %{buildroot}/%{_datadir}/zsh/site-functions/ +install -Dpm 0644 completions/fish/%{name}.fish -t %{buildroot}/%{_datadir}/fish/vendor_completions.d/ + +%files +%license LICENSE.txt +%doc README.md CHANGELOG.md +%{_bindir}/%{name} +%{_mandir}/man1/eza.1* +%{_mandir}/man5/eza_colors* +%{_datadir}/bash-completion/completions/%{name} +%{_datadir}/zsh/site-functions/_%{name} +%{_datadir}/fish/vendor_completions.d/%{name}.fish + +%changelog +* Mon Dec 16 2024 Hypercube - 0.20.21-1 +- Initial package for Hypercube diff --git a/packages/glaze/glaze.spec b/packages/glaze/glaze.spec new file mode 100644 index 0000000..df3a902 --- /dev/null +++ b/packages/glaze/glaze.spec @@ -0,0 +1,50 @@ +%global debug_package %{nil} + +Name: glaze +Version: 6.1.0 +Release: 1%{?dist} +Summary: Extremely fast, in memory, JSON and interface library for modern C++ + +License: MIT +URL: https://github.com/stephenberry/glaze +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ + +%description +Glaze is an extremely fast, in memory, JSON and interface library for modern C++. + +%package devel +Summary: Development files for %{name} +BuildArch: noarch +Provides: %{name}-static = %{version}-%{release} + +%description devel +Development files for %{name}. Glaze is a header-only library. + +%prep +%autosetup -p1 + +%build +%cmake \ + -Dglaze_INSTALL_CMAKEDIR=%{_datadir}/cmake/%{name} \ + -Dglaze_DISABLE_SIMD_WHEN_SUPPORTED:BOOL=ON \ + -Dglaze_DEVELOPER_MODE:BOOL=OFF \ + -Dglaze_ENABLE_FUZZING:BOOL=OFF +%cmake_build + +%install +%cmake_install + +%files devel +%license LICENSE +%doc README.md +%{_datadir}/cmake/%{name}/ +%{_includedir}/%{name}/ + +%changelog +* Mon Dec 16 2024 Hypercube - 6.1.0-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprcursor/hyprcursor.spec b/packages/hyprcursor/hyprcursor.spec new file mode 100644 index 0000000..2e92628 --- /dev/null +++ b/packages/hyprcursor/hyprcursor.spec @@ -0,0 +1,55 @@ +Name: hyprcursor +Version: 0.1.13 +Release: 1%{?dist} +Summary: The hyprland cursor format, library and utilities + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprcursor +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: pkgconfig(cairo) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(librsvg-2.0) +BuildRequires: pkgconfig(libzip) +BuildRequires: pkgconfig(tomlplusplus) + +%description +%{summary}. + +%package devel +Summary: Development files for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Development files for %{name}. + +%prep +%autosetup -p1 + +%build +%cmake -DCMAKE_BUILD_TYPE=Release +%cmake_build + +%install +%cmake_install + +%files +%license LICENSE +%doc README.md +%{_bindir}/hyprcursor-util +%{_libdir}/lib%{name}.so.%{version} +%{_libdir}/lib%{name}.so.0 + +%files devel +%{_includedir}/%{name}.hpp +%{_includedir}/%{name}/ +%{_libdir}/lib%{name}.so +%{_libdir}/pkgconfig/%{name}.pc + +%changelog +* Mon Dec 16 2024 Hypercube - 0.1.13-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprgraphics/hyprgraphics.spec b/packages/hyprgraphics/hyprgraphics.spec new file mode 100644 index 0000000..569cb23 --- /dev/null +++ b/packages/hyprgraphics/hyprgraphics.spec @@ -0,0 +1,67 @@ +Name: hyprgraphics +Version: 0.4.0 +Release: 1%{?dist} +Summary: Hyprland graphics / resource utilities + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprgraphics +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: pkgconfig(cairo) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(libjpeg) +BuildRequires: pkgconfig(libjxl_cms) +BuildRequires: pkgconfig(libjxl_threads) +BuildRequires: pkgconfig(libjxl) +BuildRequires: pkgconfig(libmagic) +BuildRequires: pkgconfig(libwebp) +BuildRequires: pkgconfig(pixman-1) +BuildRequires: pkgconfig(libpng) +BuildRequires: pkgconfig(pangocairo) +BuildRequires: pkgconfig(libheif) +BuildRequires: pkgconfig(librsvg-2.0) + +%description +%{summary}. + +%package devel +Summary: Development files for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Development files for %{name}. + +%prep +%autosetup -p1 + +%build +%cmake -DCMAKE_BUILD_TYPE=Release +%cmake_build + +%install +%cmake_install + +%check +%ctest + +%files +%license LICENSE +%doc README.md +%{_libdir}/lib%{name}.so.3 +%{_libdir}/lib%{name}.so.%{version} + +%files devel +%{_includedir}/%{name}/ +%{_libdir}/lib%{name}.so +%{_libdir}/pkgconfig/%{name}.pc + +%changelog +* Mon Dec 16 2024 Hypercube - 0.4.0-1 +- Update to 0.4.0 + +* Mon Dec 16 2024 Hypercube - 0.2.0-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hypridle/hypridle.spec b/packages/hypridle/hypridle.spec new file mode 100644 index 0000000..ddd283d --- /dev/null +++ b/packages/hypridle/hypridle.spec @@ -0,0 +1,68 @@ +%global sdbus_version 2.1.0 + +Name: hypridle +Version: 0.1.7 +Release: 1%{?dist} +Summary: Hyprland's idle daemon + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hypridle +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz +Source1: https://github.com/Kistler-Group/sdbus-cpp/archive/v%{sdbus_version}/sdbus-%{sdbus_version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: systemd-rpm-macros +BuildRequires: cmake(hyprwayland-scanner) +BuildRequires: pkgconfig(hyprland-protocols) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(libsystemd) +BuildRequires: pkgconfig(systemd) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-protocols) + +%description +%{summary}. + +%prep +%autosetup -p1 -a1 + +%build +pushd sdbus-cpp-%{sdbus_version} +%cmake \ + -DCMAKE_INSTALL_PREFIX=%{_builddir}/sdbus \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=OFF +%cmake_build +cmake --install %{__cmake_builddir} +popd +export PKG_CONFIG_PATH=%{_builddir}/sdbus/lib64/pkgconfig + +%cmake +%cmake_build + +%install +%cmake_install +rm %{buildroot}%{_datadir}/hypr/hypridle.conf + +%files +%license LICENSE +%doc README.md assets/example.conf +%{_bindir}/%{name} +%{_userunitdir}/%{name}.service + +%post +%systemd_user_post %{name}.service + +%preun +%systemd_user_preun %{name}.service + +%postun +%systemd_user_postun %{name}.service + +%changelog +* Mon Dec 16 2024 Hypercube - 0.1.7-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprland-guiutils/hyprland-guiutils.spec b/packages/hyprland-guiutils/hyprland-guiutils.spec new file mode 100644 index 0000000..fe306bd --- /dev/null +++ b/packages/hyprland-guiutils/hyprland-guiutils.spec @@ -0,0 +1,59 @@ +Name: hyprland-guiutils +Version: 0.2.0 +Release: 1%{?dist} +Summary: Hyprland GUI utilities (successor to hyprland-qtutils) + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprland-guiutils +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: mesa-libGLES-devel +BuildRequires: pkgconfig(aquamarine) +BuildRequires: pkgconfig(cairo) +BuildRequires: pkgconfig(egl) +BuildRequires: pkgconfig(gbm) +BuildRequires: pkgconfig(hyprgraphics) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(hyprtoolkit) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(iniparser) +BuildRequires: pkgconfig(libdrm) +BuildRequires: pkgconfig(pango) +BuildRequires: pkgconfig(pangocairo) +BuildRequires: pkgconfig(pixman-1) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-protocols) +BuildRequires: pkgconfig(xkbcommon) + +Requires: hyprtoolkit%{?_isa} + +%description +%{summary}. Includes utilities: dialog, donate-screen, run, update-screen, +and welcome. + +%prep +%autosetup -p1 + +%build +%cmake -DCMAKE_BUILD_TYPE=Release +%cmake_build + +%install +%cmake_install + +%files +%license LICENSE +%doc README.md +%{_bindir}/hyprland-dialog +%{_bindir}/hyprland-donate-screen +%{_bindir}/hyprland-run +%{_bindir}/hyprland-update-screen +%{_bindir}/hyprland-welcome + +%changelog +* Wed Dec 18 2024 Hypercube - 0.2.0-1 +- Initial package for Hypercube diff --git a/packages/hyprland-protocols/hyprland-protocols.spec b/packages/hyprland-protocols/hyprland-protocols.spec new file mode 100644 index 0000000..4aa901f --- /dev/null +++ b/packages/hyprland-protocols/hyprland-protocols.spec @@ -0,0 +1,40 @@ +Name: hyprland-protocols +Version: 0.7.0 +Release: 1%{?dist} +Summary: Wayland protocol extensions for Hyprland +BuildArch: noarch + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprland-protocols +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +BuildRequires: meson + +%description +%{summary}. + +%package devel +Summary: Wayland protocol extensions for Hyprland + +%description devel +%{summary}. + +%prep +%autosetup -p1 + +%build +%meson +%meson_build + +%install +%meson_install + +%files devel +%license LICENSE +%doc README.md +%{_datadir}/pkgconfig/%{name}.pc +%{_datadir}/%{name}/ + +%changelog +* Mon Dec 16 2024 Hypercube - 0.7.0-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprland-qt-support/hyprland-qt-support.spec b/packages/hyprland-qt-support/hyprland-qt-support.spec new file mode 100644 index 0000000..7111c7a --- /dev/null +++ b/packages/hyprland-qt-support/hyprland-qt-support.spec @@ -0,0 +1,42 @@ +Name: hyprland-qt-support +Version: 0.1.0 +Release: 1%{?dist} +Summary: A Qt6 Qml style provider for hypr* apps + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprland-qt-support +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: qt6-rpm-macros +BuildRequires: cmake(Qt6Quick) +BuildRequires: cmake(Qt6QuickControls2) +BuildRequires: cmake(Qt6Qml) +BuildRequires: pkgconfig(hyprlang) + +%description +%{summary}. + +%prep +%autosetup -p1 + +%build +%cmake -DINSTALL_QMLDIR=%{_qt6_qmldir} -DCMAKE_INSTALL_LIBDIR=%{_libdir} +%cmake_build + +%install +%cmake_install + +%files +%license LICENSE +%doc README.md +%{_libdir}/libhyprland-quick-style-impl.so +%{_libdir}/libhyprland-quick-style.so +%{_qt6_qmldir}/org/hyprland/ + +%changelog +* Mon Dec 16 2024 Hypercube - 0.1.0-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprland/hyprland.spec b/packages/hyprland/hyprland.spec new file mode 100644 index 0000000..f4ddc03 --- /dev/null +++ b/packages/hyprland/hyprland.spec @@ -0,0 +1,143 @@ +Name: hyprland +Version: 0.52.2 +Release: 1%{?dist} +Summary: Dynamic tiling Wayland compositor that doesn't sacrifice on its looks + +License: BSD-3-Clause AND BSD-2-Clause AND HPND-sell-variant AND LGPL-2.1-or-later +URL: https://github.com/hyprwm/Hyprland +Source0: %{url}/releases/download/v%{version}/source-v%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: meson +BuildRequires: glaze-static +BuildRequires: pkgconfig(aquamarine) +BuildRequires: pkgconfig(cairo) +BuildRequires: pkgconfig(egl) +BuildRequires: pkgconfig(gbm) +BuildRequires: pkgconfig(gio-2.0) +BuildRequires: pkgconfig(glesv2) +BuildRequires: pkgconfig(hwdata) +BuildRequires: pkgconfig(hyprcursor) +BuildRequires: pkgconfig(hyprgraphics) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(hyprwayland-scanner) +BuildRequires: pkgconfig(libdisplay-info) +BuildRequires: pkgconfig(libdrm) +BuildRequires: pkgconfig(libinput) >= 1.28 +BuildRequires: pkgconfig(libliftoff) +BuildRequires: pkgconfig(libseat) +BuildRequires: pkgconfig(libudev) +BuildRequires: pkgconfig(pango) +BuildRequires: pkgconfig(pangocairo) +BuildRequires: pkgconfig(pixman-1) +BuildRequires: pkgconfig(re2) +BuildRequires: pkgconfig(systemd) +BuildRequires: pkgconfig(tomlplusplus) +BuildRequires: pkgconfig(uuid) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-protocols) >= 1.45 +BuildRequires: pkgconfig(wayland-scanner) +BuildRequires: pkgconfig(wayland-server) +BuildRequires: pkgconfig(xcb-composite) +BuildRequires: pkgconfig(xcb-dri3) +BuildRequires: pkgconfig(xcb-errors) +BuildRequires: pkgconfig(xcb-ewmh) +BuildRequires: pkgconfig(xcb-icccm) +BuildRequires: pkgconfig(xcb-present) +BuildRequires: pkgconfig(xcb-render) +BuildRequires: pkgconfig(xcb-renderutil) +BuildRequires: pkgconfig(xcb-res) +BuildRequires: pkgconfig(xcb-shm) +BuildRequires: pkgconfig(xcb-util) +BuildRequires: pkgconfig(xcb-xfixes) +BuildRequires: pkgconfig(xcb-xinput) +BuildRequires: pkgconfig(xcb) +BuildRequires: pkgconfig(xcursor) +BuildRequires: pkgconfig(xkbcommon) +BuildRequires: pkgconfig(xwayland) + +Requires: xorg-x11-server-Xwayland%{?_isa} +Requires: aquamarine%{?_isa} >= 0.9.2 +Requires: hyprcursor%{?_isa} >= 0.1.13 +Requires: hyprgraphics%{?_isa} >= 0.1.6 +Requires: hyprlang%{?_isa} >= 0.6.3 +Requires: hyprutils%{?_isa} >= 0.8.4 + +Recommends: ghostty +Recommends: playerctl +Recommends: brightnessctl +Recommends: mesa-dri-drivers +Recommends: polkit +Recommends: %{name}-uwsm +Recommends: (qt5-qtwayland if qt5-qtbase-gui) +Recommends: (qt6-qtwayland if qt6-qtbase-gui) + +%description +Hyprland is a dynamic tiling Wayland compositor that doesn't sacrifice +on its looks. It supports multiple layouts, fancy effects, has a +very flexible IPC model allowing for a lot of customization, a powerful +plugin system and more. + +%package uwsm +Summary: Files for a uwsm-managed session +Requires: uwsm + +%description uwsm +Files for a uwsm-managed session. + +%package devel +Summary: Header and protocol files for %{name} +License: BSD-3-Clause +Requires: %{name}%{?_isa} = %{version}-%{release} +Requires: cpio +Requires: git-core +Requires: pkgconfig(xkbcommon) + +%description devel +%{summary}. + +%prep +%autosetup -n hyprland-source -N + +cp -p subprojects/hyprland-protocols/LICENSE LICENSE-hyprland-protocols +cp -p subprojects/udis86/LICENSE LICENSE-udis86 + +%build +%cmake \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Release \ + -DNO_TESTS=TRUE \ + -DBUILD_TESTING=FALSE +%cmake_build + +%install +%cmake_install + +%files +%license LICENSE LICENSE-udis86 LICENSE-hyprland-protocols +%{_bindir}/[Hh]yprland +%{_bindir}/hyprctl +%{_bindir}/hyprpm +%{_datadir}/hypr/ +%{_datadir}/wayland-sessions/hyprland.desktop +%{_datadir}/xdg-desktop-portal/hyprland-portals.conf +%{_mandir}/man1/hyprctl.1* +%{_mandir}/man1/Hyprland.1* +%{_datadir}/bash-completion/completions/hypr* +%{_datadir}/fish/vendor_completions.d/hypr*.fish +%{_datadir}/zsh/site-functions/_hypr* + +%files uwsm +%{_datadir}/wayland-sessions/hyprland-uwsm.desktop + +%files devel +%{_datadir}/pkgconfig/hyprland.pc +%{_includedir}/hyprland/ + +%changelog +* Mon Dec 16 2024 Hypercube - 0.52.2-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprlang/hyprlang.spec b/packages/hyprlang/hyprlang.spec new file mode 100644 index 0000000..7969725 --- /dev/null +++ b/packages/hyprlang/hyprlang.spec @@ -0,0 +1,56 @@ +Name: hyprlang +Version: 0.6.7 +Release: 1%{?dist} +Summary: The official implementation library for the hypr config language + +License: LGPL-3.0-only +URL: https://github.com/hyprwm/hyprlang +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: pkgconfig(hyprutils) + +%description +%{summary}. + +%package devel +Summary: Development files for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Development files for %{name}. + +%prep +%autosetup -p1 +sed 's/.*/%{version}/' -i VERSION + +%build +%cmake +%cmake_build + +%install +%cmake_install + +%check +%ctest + +%files +%license LICENSE +%doc README.md +%{_libdir}/libhyprlang.so.2 +%{_libdir}/libhyprlang.so.%{version} + +%files devel +%{_includedir}/hyprlang.hpp +%{_libdir}/libhyprlang.so +%{_libdir}/pkgconfig/hyprlang.pc + +%changelog +* Mon Dec 16 2024 Hypercube - 0.6.7-1 +- Update to 0.6.7 + +* Mon Dec 16 2024 Hypercube - 0.6.4-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprlock/hyprlock.spec b/packages/hyprlock/hyprlock.spec new file mode 100644 index 0000000..7e554ec --- /dev/null +++ b/packages/hyprlock/hyprlock.spec @@ -0,0 +1,72 @@ +%global sdbus_version 2.1.0 + +Name: hyprlock +Version: 0.9.2 +Release: 1%{?dist} +Summary: Hyprland's GPU-accelerated screen locking utility + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprlock +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz +Source1: https://github.com/Kistler-Group/sdbus-cpp/archive/v%{sdbus_version}/sdbus-%{sdbus_version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: cmake(hyprwayland-scanner) +BuildRequires: pkgconfig(cairo) +BuildRequires: pkgconfig(egl) +BuildRequires: pkgconfig(gbm) +BuildRequires: pkgconfig(hyprgraphics) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(libdrm) +BuildRequires: pkgconfig(libsystemd) +BuildRequires: pkgconfig(opengl) +BuildRequires: pkgconfig(pam) +BuildRequires: pkgconfig(pangocairo) +BuildRequires: pkgconfig(systemd) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-egl) +BuildRequires: pkgconfig(wayland-protocols) +BuildRequires: pkgconfig(xkbcommon) + +Provides: bundled(sdbus-cpp) = %{sdbus_version} + +%description +%{summary}. + +%prep +%autosetup -p1 +mkdir -p subprojects/sdbus-cpp +tar -xf %{SOURCE1} -C subprojects/sdbus-cpp --strip=1 + +%build +pushd subprojects/sdbus-cpp +%cmake \ + -DCMAKE_INSTALL_PREFIX=%{_builddir}/sdbus \ + -DCMAKE_BUILD_TYPE=Release \ + -DSDBUSCPP_BUILD_DOCS=OFF \ + -DBUILD_SHARED_LIBS=OFF +%cmake_build +cmake --install %{_vpath_builddir} +popd +export PKG_CONFIG_PATH=%{_builddir}/sdbus/%{_lib}/pkgconfig + +%cmake -DCMAKE_BUILD_TYPE=Release +%cmake_build + +%install +%cmake_install +rm %{buildroot}%{_datadir}/hypr/%{name}.conf + +%files +%license LICENSE +%doc README.md assets/example.conf +%{_bindir}/%{name} +%config(noreplace) %{_sysconfdir}/pam.d/%{name} + +%changelog +* Mon Dec 16 2024 Hypercube - 0.9.2-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprpaper/hyprpaper.spec b/packages/hyprpaper/hyprpaper.spec new file mode 100644 index 0000000..6a41b09 --- /dev/null +++ b/packages/hyprpaper/hyprpaper.spec @@ -0,0 +1,56 @@ +Name: hyprpaper +Version: 0.7.6 +Release: 1%{?dist} +Summary: Blazing fast wayland wallpaper utility with IPC controls + +License: BSD-3-Clause AND HPND-sell-variant +URL: https://github.com/hyprwm/hyprpaper +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: systemd-rpm-macros +BuildRequires: pkgconfig(cairo) +BuildRequires: pkgconfig(glesv2) +BuildRequires: pkgconfig(hyprgraphics) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(hyprwayland-scanner) +BuildRequires: pkgconfig(libmagic) +BuildRequires: pkgconfig(pango) +BuildRequires: pkgconfig(pangocairo) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-protocols) + +%description +Hyprpaper is a blazing fast wallpaper utility for Hyprland with the ability +to dynamically change wallpapers through sockets. It will work on all +wlroots-based compositors, though. + +%prep +%autosetup -p1 + +%build +%cmake +%cmake_build + +%install +%cmake_install + +%post +%systemd_user_post %{name}.service + +%preun +%systemd_user_preun %{name}.service + +%files +%license LICENSE +%doc README.md +%{_bindir}/%{name} +%{_userunitdir}/%{name}.service + +%changelog +* Mon Dec 16 2024 Hypercube - 0.7.6-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprpolkitagent/hyprpolkitagent.spec b/packages/hyprpolkitagent/hyprpolkitagent.spec new file mode 100644 index 0000000..0766788 --- /dev/null +++ b/packages/hyprpolkitagent/hyprpolkitagent.spec @@ -0,0 +1,53 @@ +Name: hyprpolkitagent +Version: 0.1.3 +Release: 1%{?dist} +Summary: A simple polkit authentication agent for Hyprland + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprpolkitagent +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: desktop-file-utils +BuildRequires: gcc-c++ +BuildRequires: systemd-rpm-macros +BuildRequires: cmake(Qt6Quick) +BuildRequires: cmake(Qt6QuickControls2) +BuildRequires: cmake(Qt6Widgets) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(polkit-agent-1) +BuildRequires: pkgconfig(polkit-qt6-1) + +Requires: hyprland-qt-support%{?_isa} + +%description +A simple polkit authentication agent for Hyprland, written in QT/QML. + +%prep +%autosetup -p1 + +%build +%cmake +%cmake_build + +%install +%cmake_install + +%post +%systemd_user_post %{name}.service + +%preun +%systemd_user_preun %{name}.service + +%files +%license LICENSE +%doc README.md +%{_datadir}/dbus-1/services/org.hyprland.%{name}.service +%{_libexecdir}/%{name} +%{_userunitdir}/%{name}.service + +%changelog +* Mon Dec 16 2024 Hypercube - 0.1.3-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprtoolkit/hyprtoolkit.spec b/packages/hyprtoolkit/hyprtoolkit.spec new file mode 100644 index 0000000..14d3937 --- /dev/null +++ b/packages/hyprtoolkit/hyprtoolkit.spec @@ -0,0 +1,65 @@ +Name: hyprtoolkit +Version: 0.4.1 +Release: 1%{?dist} +Summary: A modern C++ Wayland-native GUI toolkit + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprtoolkit +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: mesa-libGLES-devel +BuildRequires: pkgconfig(aquamarine) +BuildRequires: pkgconfig(cairo) +BuildRequires: pkgconfig(egl) +BuildRequires: pkgconfig(gbm) +BuildRequires: pkgconfig(hyprgraphics) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(hyprwayland-scanner) +BuildRequires: pkgconfig(iniparser) +BuildRequires: pkgconfig(libdrm) +BuildRequires: pkgconfig(pango) +BuildRequires: pkgconfig(pangocairo) +BuildRequires: pkgconfig(pixman-1) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-protocols) +BuildRequires: pkgconfig(xkbcommon) + +%description +%{summary}. + +%package devel +Summary: Development files for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Development files for %{name}. + +%prep +%autosetup -p1 + +%build +%cmake -DCMAKE_BUILD_TYPE=Release +%cmake_build + +%install +%cmake_install + +%files +%license LICENSE +%doc README.md +%{_libdir}/lib%{name}.so.4 +%{_libdir}/lib%{name}.so.%{version} + +%files devel +%{_includedir}/%{name}/ +%{_libdir}/lib%{name}.so +%{_libdir}/pkgconfig/%{name}.pc + +%changelog +* Wed Dec 18 2024 Hypercube - 0.4.1-1 +- Initial package for Hypercube diff --git a/packages/hyprutils/hyprutils.spec b/packages/hyprutils/hyprutils.spec new file mode 100644 index 0000000..b783a37 --- /dev/null +++ b/packages/hyprutils/hyprutils.spec @@ -0,0 +1,55 @@ +Name: hyprutils +Version: 0.11.0 +Release: 1%{?dist} +Summary: Hyprland utilities library used across the ecosystem + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprutils +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: pkgconfig(pixman-1) + +%description +%{summary}. + +%package devel +Summary: Development files for %{name} +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description devel +Development files for %{name}. + +%prep +%autosetup -p1 + +%build +%cmake +%cmake_build + +%install +%cmake_install + +%check +%ctest + +%files +%license LICENSE +%doc README.md +%{_libdir}/lib%{name}.so.%{version} +%{_libdir}/lib%{name}.so.10 + +%files devel +%{_includedir}/%{name}/ +%{_libdir}/lib%{name}.so +%{_libdir}/pkgconfig/%{name}.pc + +%changelog +* Mon Dec 16 2024 Hypercube - 0.11.0-1 +- Update to 0.11.0 (adds cli/Logger.hpp needed by aquamarine) + +* Mon Dec 16 2024 Hypercube - 0.10.0-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/hyprwayland-scanner/hyprwayland-scanner.spec b/packages/hyprwayland-scanner/hyprwayland-scanner.spec new file mode 100644 index 0000000..149689e --- /dev/null +++ b/packages/hyprwayland-scanner/hyprwayland-scanner.spec @@ -0,0 +1,44 @@ +Name: hyprwayland-scanner +Version: 0.4.5 +Release: 1%{?dist} +Summary: A Hyprland implementation of wayland-scanner, in and for C++ + +License: BSD-3-Clause +URL: https://github.com/hyprwm/hyprwayland-scanner +Source: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +ExcludeArch: %{ix86} + +BuildRequires: cmake +BuildRequires: cmake(pugixml) +BuildRequires: gcc-c++ + +%description +%{summary}. + +%package devel +Summary: A Hyprland implementation of wayland-scanner, in and for C++ + +%description devel +%{summary}. + +%prep +%autosetup -p1 + +%build +%cmake +%cmake_build + +%install +%cmake_install + +%files devel +%license LICENSE +%doc README.md +%{_bindir}/%{name} +%{_libdir}/pkgconfig/%{name}.pc +%{_libdir}/cmake/%{name}/ + +%changelog +* Mon Dec 16 2024 Hypercube - 0.4.5-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/packages/lazygit/lazygit.spec b/packages/lazygit/lazygit.spec new file mode 100644 index 0000000..5e92145 --- /dev/null +++ b/packages/lazygit/lazygit.spec @@ -0,0 +1,39 @@ +# NOTE: This package requires "Enable internet access during builds" in COPR settings +# because go needs to download dependencies + +%global debug_package %{nil} + +Name: lazygit +Version: 0.57.0 +Release: 1%{?dist} +Summary: Simple terminal UI for git commands + +License: MIT +URL: https://github.com/jesseduffield/lazygit +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +BuildRequires: golang >= 1.21 +BuildRequires: git + +%description +A simple terminal UI for git commands. Lazygit makes it easier to add files, +resolve merge conflicts, checkout recent branches, scroll through logs/diffs +and more. It's designed to make git less painful. + +%prep +%autosetup -n %{name}-%{version} + +%build +go build -ldflags "-X main.version=%{version}" -o %{name} + +%install +install -Dpm 0755 %{name} %{buildroot}%{_bindir}/%{name} + +%files +%license LICENSE +%doc README.md +%{_bindir}/%{name} + +%changelog +* Mon Dec 16 2024 Hypercube - 0.57.0-1 +- Initial package for Hypercube diff --git a/packages/livesys-scripts/livesys-scripts.spec b/packages/livesys-scripts/livesys-scripts.spec new file mode 100644 index 0000000..5a6e614 --- /dev/null +++ b/packages/livesys-scripts/livesys-scripts.spec @@ -0,0 +1,66 @@ +Name: livesys-scripts +Version: 0.9.1 +Release: 1.hypercube%{?dist} +Summary: Scripts for auto-configuring live media during boot (with Hyprland support) + +License: GPL-3.0-or-later +URL: https://github.com/binarypie-dev/livesys-scripts +Source: https://github.com/binarypie-dev/livesys-scripts/archive/refs/heads/main.tar.gz + +BuildRequires: systemd-rpm-macros +BuildRequires: make + +BuildArch: noarch + +# This package provides livesys-scripts with Hyprland support +Provides: livesys-scripts = %{version}-%{release} +Obsoletes: livesys-scripts < %{version}-%{release} + +%description +Scripts for auto-configuring live media during boot. +This version includes Hyprland session support. + + +%prep +%autosetup -n %{name}-main -p1 + + +%build +# Nothing to do + +%install +%make_install + +# Make ghost files +mkdir -p %{buildroot}%{_sharedstatedir}/livesys +touch %{buildroot}%{_sharedstatedir}/livesys/livesys-session-extra +touch %{buildroot}%{_sharedstatedir}/livesys/livesys-session-late-extra + + +%preun +%systemd_preun livesys.service livesys-late.service + + +%post +%systemd_post livesys.service livesys-late.service + + +%postun +%systemd_postun livesys.service livesys-late.service + + +%files +%license COPYING +%doc README.md +%config(noreplace) %{_sysconfdir}/sysconfig/livesys +%{_libexecdir}/livesys/ +%{_unitdir}/livesys* +%dir %{_sharedstatedir}/livesys +%ghost %{_sharedstatedir}/livesys/livesys-session-extra +%ghost %{_sharedstatedir}/livesys/livesys-session-late-extra + + +%changelog +* Mon Dec 16 2024 Hypercube - 0.9.1-1.hypercube +- Add Hyprland session support +- Based on upstream 0.9.1 diff --git a/packages/quickshell/quickshell.spec b/packages/quickshell/quickshell.spec new file mode 100644 index 0000000..5a81a89 --- /dev/null +++ b/packages/quickshell/quickshell.spec @@ -0,0 +1,82 @@ +Name: quickshell +Version: 0.2.1 +Release: 1%{?dist} +Summary: Flexible QtQuick based desktop shell toolkit + +License: LGPL-3.0-or-later +URL: https://github.com/quickshell-mirror/quickshell +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +BuildRequires: cmake +BuildRequires: ninja-build +BuildRequires: gcc-c++ +BuildRequires: pkg-config +BuildRequires: qt6-qtbase-devel >= 6.6.0 +BuildRequires: qt6-qtdeclarative-devel +BuildRequires: qt6-qtshadertools-devel +BuildRequires: qt6-qtsvg-devel +BuildRequires: qt6-qtwayland-devel +BuildRequires: spirv-tools-devel +BuildRequires: cli11-devel + +# Wayland support +BuildRequires: wayland-devel +BuildRequires: wayland-protocols-devel + +# X11 support +BuildRequires: libxcb-devel + +# Audio/PipeWire +BuildRequires: pipewire-devel + +# DBus +BuildRequires: qt6-qtbase-private-devel + +# Optional dependencies +BuildRequires: jemalloc-devel +BuildRequires: pam-devel +BuildRequires: polkit-devel +BuildRequires: glib2-devel +BuildRequires: libdrm-devel +BuildRequires: mesa-libgbm-devel + +Requires: qt6-qtbase >= 6.6.0 +Requires: qt6-qtdeclarative +Requires: qt6-qtwayland +Requires: qt6-qtsvg + +%description +Quickshell is a toolkit for constructing desktop components like status bars, +widgets, and lockscreens using QtQuick on Wayland compositors or window +managers. It supports Hyprland, Sway, and other Wayland compositors. + +%prep +%autosetup -n %{name}-%{version} + +%build +%cmake \ + -GNinja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DWAYLAND=ON \ + -DX11=ON \ + -DPIPEWIRE=ON \ + -DHYPRLAND=ON \ + -DI3=ON \ + -DCRASH_REPORTER=OFF + +%cmake_build + +%install +%cmake_install + +%files +%license LICENSE LICENSE-GPL +%doc README.md +%{_bindir}/%{name} +%{_bindir}/qs +%{_datadir}/applications/org.quickshell.desktop +%{_datadir}/icons/hicolor/scalable/apps/org.quickshell.svg + +%changelog +* Mon Dec 16 2024 Hypercube - 0.2.1-1 +- Initial package for Hypercube diff --git a/packages/starship/starship.spec b/packages/starship/starship.spec new file mode 100644 index 0000000..f2669e4 --- /dev/null +++ b/packages/starship/starship.spec @@ -0,0 +1,58 @@ +%global debug_package %{nil} + +Name: starship +Version: 1.24.1 +Release: 1%{?dist} +Summary: Minimal, blazing-fast, and infinitely customizable prompt for any shell + +License: ISC +URL: https://github.com/starship/starship +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz + +BuildRequires: cargo >= 1.80 +BuildRequires: rust >= 1.80 +BuildRequires: gcc +BuildRequires: cmake3 +BuildRequires: pkgconfig(openssl) +BuildRequires: pkgconfig(zlib) + +%description +Starship is the minimal, blazing-fast, and infinitely customizable prompt for +any shell! The prompt shows information you need while you're working, while +staying sleek and out of the way. It works with Bash, Zsh, Fish, PowerShell, +Ion, Elvish, Tcsh, Xonsh, Nushell, and Cmd. + +%prep +%autosetup -n %{name}-%{version} + +%build +# Build handled in install + +%install +export CARGO_PROFILE_RELEASE_BUILD_OVERRIDE_OPT_LEVEL=3 +export CMAKE=cmake3 +RUSTFLAGS='-C strip=symbols' cargo install --root=%{buildroot}%{_prefix} --path=. + +# Generate and install shell completions +mkdir -p %{buildroot}%{_datadir}/bash-completion/completions +mkdir -p %{buildroot}%{_datadir}/zsh/site-functions +mkdir -p %{buildroot}%{_datadir}/fish/vendor_completions.d +%{buildroot}%{_bindir}/%{name} completions bash > %{buildroot}%{_datadir}/bash-completion/completions/%{name} +%{buildroot}%{_bindir}/%{name} completions zsh > %{buildroot}%{_datadir}/zsh/site-functions/_%{name} +%{buildroot}%{_bindir}/%{name} completions fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/%{name}.fish + +# Remove .crates.toml and .crates2.json created by cargo install +rm -f %{buildroot}%{_prefix}/.crates.toml +rm -f %{buildroot}%{_prefix}/.crates2.json + +%files +%license LICENSE +%doc README.md CHANGELOG.md +%{_bindir}/%{name} +%{_datadir}/bash-completion/completions/%{name} +%{_datadir}/zsh/site-functions/_%{name} +%{_datadir}/fish/vendor_completions.d/%{name}.fish + +%changelog +* Mon Dec 16 2024 Hypercube - 1.24.1-1 +- Initial package for Hypercube diff --git a/packages/uwsm/uwsm.spec b/packages/uwsm/uwsm.spec new file mode 100644 index 0000000..1defefb --- /dev/null +++ b/packages/uwsm/uwsm.spec @@ -0,0 +1,83 @@ +Name: uwsm +Version: 0.23.3 +Release: 1%{?dist} +Summary: Universal Wayland Session Manager + +License: MIT +URL: https://github.com/Vladimir-csp/uwsm +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz +BuildArch: noarch + +BuildRequires: desktop-file-utils +BuildRequires: meson +BuildRequires: python-rpm-macros +BuildRequires: python3 +BuildRequires: python3-dbus +BuildRequires: python3-pyxdg +BuildRequires: scdoc +BuildRequires: systemd-rpm-macros + +Requires: python3 +Requires: python3-dbus +Requires: python3-pyxdg +Requires: util-linux + +Recommends: /usr/bin/notify-send +Recommends: /usr/bin/whiptail +Recommends: wofi + +%description +Wraps standalone Wayland compositors into a set of Systemd units on the fly. +This provides robust session management including environment, XDG autostart +support, bi-directional binding with login session, and clean shutdown. +For compositors this is an opportunity to offload Systemd integration and +session/XDG autostart management in Systemd-managed environments. + +%prep +%autosetup -p1 + +%build +%meson -Duuctl=enabled -Dfumon=enabled -Duwsm-app=enabled +%meson_build + +%install +%meson_install +%py_byte_compile %{python3} %{buildroot}%{_datadir}/%{name}/modules + +%check +desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop + +%post +%systemd_user_post fumon.service + +%preun +%systemd_user_preun fumon.service + +%postun +%systemd_user_postun fumon.service + +%files +%doc %{_docdir}/%{name}/ +%license LICENSE +%{_bindir}/%{name} +%{_bindir}/%{name}-app +%{_bindir}/%{name}-terminal +%{_bindir}/%{name}-terminal-scope +%{_bindir}/%{name}-terminal-service +%{_bindir}/fumon +%{_bindir}/uuctl +%{_datadir}/%{name}/ +%{_datadir}/applications/uuctl.desktop +%{_mandir}/man1/%{name}.1.* +%{_mandir}/man1/fumon.1.* +%{_mandir}/man1/uuctl.1.* +%{_mandir}/man1/uwsm-app.1.* +%{_mandir}/man3/%{name}-plugins.3.* +%{_userunitdir}/fumon.service +%{_userunitdir}/*-graphical.slice +%{_userunitdir}/wayland-*.service +%{_userunitdir}/wayland-*.target + +%changelog +* Mon Dec 16 2024 Hypercube - 0.23.3-1 +- Initial package for Hypercube (based on solopasha/hyprland COPR) diff --git a/packages/xdg-desktop-portal-hyprland/xdg-desktop-portal-hyprland.spec b/packages/xdg-desktop-portal-hyprland/xdg-desktop-portal-hyprland.spec new file mode 100644 index 0000000..958d3c9 --- /dev/null +++ b/packages/xdg-desktop-portal-hyprland/xdg-desktop-portal-hyprland.spec @@ -0,0 +1,73 @@ +%global sdbus_version 2.1.0 + +Name: xdg-desktop-portal-hyprland +Epoch: 1 +Version: 1.3.11 +Release: 1%{?dist} +Summary: xdg-desktop-portal backend for hyprland + +License: BSD-3-Clause +URL: https://github.com/hyprwm/xdg-desktop-portal-hyprland +Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz +Source1: https://github.com/Kistler-Group/sdbus-cpp/archive/v%{sdbus_version}/sdbus-%{sdbus_version}.tar.gz + +BuildRequires: cmake +BuildRequires: gcc-c++ +BuildRequires: systemd-rpm-macros +BuildRequires: pkgconfig(gbm) +BuildRequires: pkgconfig(hyprland-protocols) +BuildRequires: pkgconfig(hyprlang) +BuildRequires: pkgconfig(hyprutils) +BuildRequires: pkgconfig(hyprwayland-scanner) +BuildRequires: pkgconfig(libdrm) +BuildRequires: pkgconfig(libpipewire-0.3) +BuildRequires: pkgconfig(libsystemd) +BuildRequires: pkgconfig(Qt6Widgets) +BuildRequires: pkgconfig(systemd) +BuildRequires: pkgconfig(wayland-client) +BuildRequires: pkgconfig(wayland-protocols) +BuildRequires: pkgconfig(wayland-scanner) + +Requires: dbus +Requires: grim +Requires: xdg-desktop-portal +Requires: slurp +Requires: qt6-qtwayland + +Enhances: hyprland +Supplements: hyprland + +Provides: bundled(sdbus-cpp) = %{sdbus_version} + +%description +%{summary}. + +%prep +%autosetup -p1 +tar -xf %{SOURCE1} -C subprojects/sdbus-cpp --strip=1 + +%build +%cmake -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release +%cmake_build + +%install +%cmake_install + +%post +%systemd_user_post %{name}.service + +%preun +%systemd_user_preun %{name}.service + +%files +%license LICENSE +%doc README.md +%{_bindir}/hyprland-share-picker +%{_datadir}/dbus-1/services/org.freedesktop.impl.portal.desktop.hyprland.service +%{_datadir}/xdg-desktop-portal/portals/hyprland.portal +%{_libexecdir}/%{name} +%{_userunitdir}/%{name}.service + +%changelog +* Mon Dec 16 2024 Hypercube - 1.3.11-1 +- Initial package for Hypercube (based on sdegler/hyprland COPR) diff --git a/system_files/shared/etc/dconf/db/gdm.d/01-hypercube b/system_files/shared/etc/dconf/db/gdm.d/01-hypercube deleted file mode 100644 index 4fa4003..0000000 --- a/system_files/shared/etc/dconf/db/gdm.d/01-hypercube +++ /dev/null @@ -1,18 +0,0 @@ -[org/gnome/login-screen] -disable-user-list=false - -[org/gnome/desktop/interface] -color-scheme='prefer-dark' - -[org/gnome/desktop/screensaver] -picture-uri='file:///usr/share/backgrounds/hypercube/background.png' -picture-options='zoom' -primary-color='#1a1b26' -secondary-color='#1a1b26' - -[org/gnome/desktop/background] -picture-uri='file:///usr/share/backgrounds/hypercube/background.png' -picture-uri-dark='file:///usr/share/backgrounds/hypercube/background.png' -picture-options='zoom' -primary-color='#1a1b26' -secondary-color='#1a1b26' diff --git a/system_files/shared/etc/dconf/db/local.d/01-hypercube-dark-mode b/system_files/shared/etc/dconf/db/local.d/01-hypercube-dark-mode index 45883e7..1caee5a 100644 --- a/system_files/shared/etc/dconf/db/local.d/01-hypercube-dark-mode +++ b/system_files/shared/etc/dconf/db/local.d/01-hypercube-dark-mode @@ -1,5 +1,5 @@ # Hypercube Settings -# System-wide defaults for GNOME desktop +# System-wide defaults for GTK applications running under Hyprland [org/gnome/desktop/interface] color-scheme='prefer-dark' @@ -7,9 +7,4 @@ gtk-theme='Tokyonight-Dark-BL' icon-theme='Tokyonight-Dark' cursor-theme='Adwaita' -[org/gnome/desktop/wm/preferences] -theme='Tokyonight-Dark-BL' - -[org/gnome/desktop/input-sources] -# XKB options: Caps Lock as Ctrl, swap Alt and Super -xkb-options=['caps:ctrl_modifier', 'altwin:swap_alt_win'] +# Note: XKB options are now configured in Hyprland (hyprland.conf) diff --git a/system_files/shared/etc/dconf/profile/gdm b/system_files/shared/etc/dconf/profile/gdm deleted file mode 100644 index e4ac220..0000000 --- a/system_files/shared/etc/dconf/profile/gdm +++ /dev/null @@ -1,2 +0,0 @@ -user-db:user -system-db:gdm diff --git a/system_files/shared/etc/distrobox/distrobox.ini b/system_files/shared/etc/distrobox/distrobox.ini new file mode 100644 index 0000000..c274dad --- /dev/null +++ b/system_files/shared/etc/distrobox/distrobox.ini @@ -0,0 +1,11 @@ +# Hypercube Distrobox Configuration +# Pre-configured containers for development + +[bluefin-cli] +image=ghcr.io/ublue-os/bluefin-cli:latest +pull=true +init=false +start_now=false +# Homebrew and common CLI tools pre-installed +# Access via: distrobox enter bluefin-cli +# Or use the 'brew' alias configured in fish diff --git a/system_files/shared/etc/greetd/config.toml b/system_files/shared/etc/greetd/config.toml new file mode 100644 index 0000000..b047317 --- /dev/null +++ b/system_files/shared/etc/greetd/config.toml @@ -0,0 +1,8 @@ +[terminal] +# The VT to run the greeter on +vt = 1 + +[default_session] +# Run tuigreet with Hyprland as the default session +command = "tuigreet --remember --remember-session --time --greeting 'Welcome to Hypercube' --asterisks --cmd Hyprland" +user = "greeter" diff --git a/system_files/shared/usr/lib/systemd/system/hypercube-first-boot.service b/system_files/shared/usr/lib/systemd/system/hypercube-first-boot.service new file mode 100644 index 0000000..7d8d640 --- /dev/null +++ b/system_files/shared/usr/lib/systemd/system/hypercube-first-boot.service @@ -0,0 +1,16 @@ +[Unit] +Description=Hypercube First Boot User Setup +After=systemd-user-sessions.service plymouth-quit-wait.service graphical.target +Before=greetd.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/libexec/hypercube/first-boot-wizard +# Run as root but with display access +Environment=XDG_RUNTIME_DIR=/run/user/0 +Environment=WAYLAND_DISPLAY=wayland-0 + +[Install] +WantedBy=multi-user.target +RequiredBy=greetd.service diff --git a/system_files/shared/usr/libexec/hypercube/first-boot-wizard b/system_files/shared/usr/libexec/hypercube/first-boot-wizard new file mode 100755 index 0000000..430085a --- /dev/null +++ b/system_files/shared/usr/libexec/hypercube/first-boot-wizard @@ -0,0 +1,74 @@ +#!/bin/bash +# Hypercube First Boot Wizard Launcher +# Launches the graphical quickshell welcome wizard for user creation +# SPDX-License-Identifier: GPL-3.0-or-later + +set -euo pipefail + +# Check if any regular users exist (UID >= 1000) +if getent passwd | awk -F: '$3 >= 1000 && $3 < 65534 {exit 1}'; then + # No regular users exist, run the wizard + : +else + # Users already exist, disable this service and exit + systemctl disable hypercube-first-boot.service 2>/dev/null || true + exit 0 +fi + +# Start a minimal Hyprland session for the wizard +# This runs before greetd, so we need to start our own compositor + +# Create a temporary Hyprland config for the wizard +WIZARD_CONFIG=$(mktemp) +cat > "$WIZARD_CONFIG" << 'EOF' +# Minimal Hyprland config for first-boot wizard +monitor=,preferred,auto,1 + +input { + kb_layout = us + follow_mouse = 1 + touchpad { + natural_scroll = yes + } +} + +general { + gaps_in = 0 + gaps_out = 0 + border_size = 0 +} + +decoration { + rounding = 0 + blur { + enabled = false + } +} + +animations { + enabled = no +} + +# Start quickshell in welcome mode +exec-once = quickshell -c /etc/hypercube/quickshell/welcome-mode.qml + +# Auto-exit when wizard completes +exec-once = sleep 1 && while pgrep -x quickshell > /dev/null; do sleep 1; done && hyprctl dispatch exit +EOF + +# Set up the display +export XDG_SESSION_TYPE=wayland +export XDG_CURRENT_DESKTOP=Hyprland +export XDG_SESSION_DESKTOP=Hyprland + +# Run Hyprland with the wizard config +# This will block until the wizard is complete +Hyprland -c "$WIZARD_CONFIG" 2>/dev/null || true + +# Clean up +rm -f "$WIZARD_CONFIG" + +# Disable this service so it doesn't run again +systemctl disable hypercube-first-boot.service 2>/dev/null || true + +exit 0 diff --git a/system_files/shared/usr/share/ublue-os/just/60-custom.just b/system_files/shared/usr/share/ublue-os/just/60-custom.just index 3d6aed2..98b6691 100644 --- a/system_files/shared/usr/share/ublue-os/just/60-custom.just +++ b/system_files/shared/usr/share/ublue-os/just/60-custom.just @@ -14,7 +14,6 @@ hypercube-config-reset: echo "" echo "Configs that will be reset:" echo " - Ghostty (~/.config/ghostty/)" - echo " - Wezterm (~/.config/wezterm/)" echo "" echo "You can restore your old configs from the backup directory at any time." echo "" @@ -49,7 +48,6 @@ hypercube-config-reset: # Backup existing configs backup_and_reset "ghostty" "Ghostty" - backup_and_reset "wezterm" "Wezterm" echo "" echo "Creating Hypercube config stubs..." @@ -67,22 +65,6 @@ config-file = /usr/share/hypercube/config/ghostty/config EOF echo " Created Ghostty config stub" - # Create Wezterm stub - mkdir -p "$HOME/.config/wezterm" - cat > "$HOME/.config/wezterm/wezterm.lua" << 'EOF' --- Hypercube Wezterm Configuration --- System defaults are loaded below. Add your customizations after this line. --- To replace defaults entirely, remove the dofile line and start fresh. - -local config = dofile("/usr/share/hypercube/config/wezterm/wezterm.lua") - --- Your customizations below: --- Example: config.font_size = 14 - -return config -EOF - echo " Created Wezterm config stub" - echo "" echo "Done! Your old configs have been backed up to:" echo " $BACKUP_DIR" diff --git a/system_files/shared/usr/share/ublue-os/just/61-dx.just b/system_files/shared/usr/share/ublue-os/just/61-dx.just new file mode 100644 index 0000000..ab1283b --- /dev/null +++ b/system_files/shared/usr/share/ublue-os/just/61-dx.just @@ -0,0 +1,98 @@ +# Hypercube DX (Developer Experience) Commands + +# Enter the bluefin-cli container (Homebrew environment) +brew: + #!/usr/bin/bash + set -euo pipefail + + # Check if bluefin-cli container exists + if ! distrobox list | grep -q "bluefin-cli"; then + echo "Creating bluefin-cli container..." + distrobox assemble create --file /etc/distrobox/distrobox.ini + fi + + distrobox enter bluefin-cli + +# Run a brew command directly without entering the container +brew-run *args: + #!/usr/bin/bash + set -euo pipefail + + # Check if bluefin-cli container exists + if ! distrobox list | grep -q "bluefin-cli"; then + echo "Creating bluefin-cli container..." + distrobox assemble create --file /etc/distrobox/distrobox.ini + fi + + distrobox enter bluefin-cli -- brew {{ args }} + +# Create distrobox containers from config +distrobox-assemble: + #!/usr/bin/bash + set -euo pipefail + + echo "Creating distrobox containers from /etc/distrobox/distrobox.ini..." + distrobox assemble create --file /etc/distrobox/distrobox.ini + echo "" + echo "Containers created. Use 'distrobox list' to see them." + +# List all distrobox containers +distrobox-list: + distrobox list + +# Create a new dev container with common tools +distrobox-dev name: + #!/usr/bin/bash + set -euo pipefail + + echo "Creating development container: {{ name }}" + distrobox create --name "{{ name }}" --image ghcr.io/ublue-os/bluefin-cli:latest + echo "" + echo "Container created. Enter with: distrobox enter {{ name }}" + +# Create a Fedora container +distrobox-fedora name: + #!/usr/bin/bash + set -euo pipefail + + echo "Creating Fedora container: {{ name }}" + distrobox create --name "{{ name }}" --image registry.fedoraproject.org/fedora:latest + echo "" + echo "Container created. Enter with: distrobox enter {{ name }}" + +# Create an Ubuntu container +distrobox-ubuntu name: + #!/usr/bin/bash + set -euo pipefail + + echo "Creating Ubuntu container: {{ name }}" + distrobox create --name "{{ name }}" --image docker.io/library/ubuntu:latest + echo "" + echo "Container created. Enter with: distrobox enter {{ name }}" + +# Create an Arch Linux container +distrobox-arch name: + #!/usr/bin/bash + set -euo pipefail + + echo "Creating Arch Linux container: {{ name }}" + distrobox create --name "{{ name }}" --image docker.io/library/archlinux:latest + echo "" + echo "Container created. Enter with: distrobox enter {{ name }}" + +# Show developer environment status +dx-status: + #!/usr/bin/bash + set -euo pipefail + + echo "Hypercube DX Status" + echo "===================" + echo "" + + echo "Container Runtimes:" + echo -n " Podman: " + podman --version 2>/dev/null || echo "not found" + echo "" + + echo "Distrobox Containers:" + distrobox list 2>/dev/null || echo " (none)"