1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00

Compare commits

..

70 Commits

Author SHA1 Message Date
bbedward 9673078a75 fix gui 2025-12-15 16:30:35 -05:00
bbedward 9e8c93bfd7 displays: fix hyprland config saving 2025-12-15 16:04:31 -05:00
bbedward 43d6f4b1d3 displays: add configurator (Beta)
- Position, resolution, refresh, orientation, VRR
- niri, Hyprland, MangoWC
- Rely on wlr-output for reading data, compositors to write output
  configurations
- Re-organize display setting group
2025-12-15 15:55:31 -05:00
bbedward bafe1c5fee niri: handle window urgency event
fixes #1033
2025-12-15 12:16:43 -05:00
bbedward 306d7b2ce0 gamma: guard against application
- QML will sync its desired state with GO, when IE settings are changed
  or opened. Go was applying gamma even if unchanged
- Track last applied gamma to avoid sends
2025-12-15 11:43:16 -05:00
bbedward e9f6583c60 workspaces: add scroll handler to widget itself 2025-12-15 11:12:27 -05:00
redybcs 42a2835929 Update flake.nix to fix Hash Mismatch (#1035)
Looks like there hasn't been any go.mod updates since the workflow to fix the hash was repaired.
2025-12-15 14:14:29 +01:00
purian23 c2c90c680e distro: OBS edgecase 2025-12-15 01:28:00 -05:00
purian23 cd01f6378c Revise OBS / PPA Workflows 2025-12-15 00:37:42 -05:00
purian23 6033075de6 distro: Revise builds to use API variants 2025-12-15 00:32:40 -05:00
tsukasa 79794d3441 dankmodal: removed backgroundWindow to fix clicking twice (#1030)
* dankmodal: removed backgroundWindow

removed 'backgroundWindow' but combined it with 'contentWindow'

* made single window behavior specific to hyprland

this should keep other compositor behavior the same and fix double
clicking to exit out of Spotlight/ClipboardHist/Powermenu
2025-12-14 19:52:06 -05:00
bbedward 031f86b417 Revert "Fixed having to click twice to exit out of Spotlight/Cliphist/Powermenu (#1022)"
This reverts commit ca5fe6f7db.
2025-12-14 19:09:04 -05:00
bbedward 891f53cf6f battery: fix button group sclaing 2025-12-14 17:22:54 -05:00
bbedward 848991cf5b idle: implement screensaver interface
- Mainly used to create the idle inhibitor when an app requests
  screensaver inhibit
2025-12-14 16:49:59 -05:00
bbedward d37ddd1d41 vpn: optim cc and dankbar widget 2025-12-14 16:12:46 -05:00
Pi Home Server 00d12acd5e Add hide option for updater widget (#1028) 2025-12-14 15:55:47 -05:00
bbedward 3bbc78a44f dankbar: make control center widget per-instance not global
fixes #1017
2025-12-14 15:52:46 -05:00
bbedward b0a6652cc6 ci: simplify changelog handling 2025-12-14 14:23:27 -05:00
bbedward cb710b2e5f notifications: fix redundant height animation 2025-12-14 13:40:21 -05:00
tsukasa ca5fe6f7db Fixed having to click twice to exit out of Spotlight/Cliphist/Powermenu (#1022)
There's possibly more but this fix the need of having to click the
background twice to close those modals.
2025-12-14 11:16:25 -05:00
bbedward fb75f4c68b lock/greeter: fix font alignment
fixes #1018
2025-12-14 11:13:48 -05:00
bbedward 5e2a418485 binds: fix to scale with arbitrary font sizes 2025-12-14 10:56:03 -05:00
bbedward 24fe215067 ci: pull changelogs from obs/launchpad APIs
- Get changelog from OBS/Launchpad API endpoints, instead of storing in
  git
2025-12-14 10:42:00 -05:00
bbedward ab2e8875ac runningapps: round icon margin to integer 2025-12-14 10:25:36 -05:00
dms-ci[bot] dec5740c74 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 15:22:12 +00:00
bbedward 208266dfa3 dwl: fix layout popout 2025-12-14 10:17:58 -05:00
dms-ci[bot] 32f218d58c ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 04:07:07 +00:00
dms-ci[bot] 6fdaab2ccd ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 03:58:50 +00:00
purian23 d336866f44 distro: Let the workflow run 2025-12-13 22:54:58 -05:00
purian23 b40df5f1c4 distro: Unify options across repos 2025-12-13 22:38:25 -05:00
dms-ci[bot] 3c9886ad1b ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-14 01:55:34 +00:00
bbedward ea205ebd12 wallpaper: pause cycling when locked, clean state when changing modes 2025-12-13 20:29:02 -05:00
bbedward 30dad46c94 dankbar: add scroll wheel behavior configuration 2025-12-13 20:12:21 -05:00
dms-ci[bot] fbf79e62e9 ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 21:23:58 +00:00
dms-ci[bot] efcf72bc08 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 21:19:59 +00:00
bbedward 3b511e2f55 i18n: add hungarian 2025-12-13 14:03:49 -05:00
dms-ci[bot] e4e20fb43a ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 15:26:22 +00:00
dms-ci[bot] 48ccff67a6 ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 15:25:01 +00:00
Souyama a783d6507b Change DPMS off to DPMS toggle in hyprland.conf (#1011) 2025-12-13 10:07:11 -05:00
bbedward fd94e60797 cava: dont set method/source 2025-12-13 10:04:20 -05:00
bbedward a1bcb7ea30 vpn: just try and import all types on errors 2025-12-13 10:02:57 -05:00
bbedward 31b67164c7 clipboard: re-add ownership option 2025-12-13 09:45:04 -05:00
bbedward 786c13f892 clipboard: fix mime type selection 2025-12-13 09:35:55 -05:00
bbedward c652659d54 wallpaper: scale texture to physical pixels
- reverts a regression
2025-12-13 08:43:46 -05:00
dms-ci[bot] ca39196f13 ci: Auto-update OBS packages [dms,dms-git]
🤖 Automated by GitHub Actions
2025-12-13 07:00:01 +00:00
dms-ci[bot] f02dd8fd4b ci: Auto-update PPA packages [dms,dms-git,dms-greeter]
🤖 Automated by GitHub Actions
2025-12-13 06:51:47 +00:00
purian23 0f89886ce7 distro: Break the loop 2025-12-13 01:44:20 -05:00
dms-ci[bot] 1118404192 ci: Auto-update PPA packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:34:59 +00:00
dms-ci[bot] f011ea6cce ci: Auto-update OBS packages [dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:30:45 +00:00
dms-ci[bot] b2ac9c6c1a ci: Auto-update OBS packages [dms,dms-git]
🤖 Automated by GitHub Actions
2025-12-13 06:06:44 +00:00
dms-ci[bot] fbab41abd6 ci: Auto-update PPA packages [dms,dms-git,dms-greeter]
🤖 Automated by GitHub Actions
2025-12-13 05:58:14 +00:00
bbedward 82f881af5b matugen: scrub the never implemented dynamic contrast palette 2025-12-13 00:51:51 -05:00
purian23 68de9b437d distro: Switch to dms-ci 2025-12-13 00:50:42 -05:00
bbedward 830a715b6d wlcontext: use poll with wake pipe instead of read deadlines 2025-12-13 00:46:30 -05:00
bbedward ce4aca9a72 fix shellcheck 2025-12-13 00:29:20 -05:00
bbedward 7641171a01 clipboard: move cl receive to main wlcontext goroutine 2025-12-13 00:16:56 -05:00
purian23 119e084e52 distro: Remove PR tests 2025-12-13 00:15:37 -05:00
bbedward 7c6d52913e niri: fix test 2025-12-12 23:57:50 -05:00
bbedward f63ab5cf7c ci: add workflow for pushing stable tag 2025-12-12 23:57:50 -05:00
purian23 50f1bc5017 distros: Remove false path dir 2025-12-12 23:52:31 -05:00
bbedward c3ab409b6a clipboard: scrap persist, optimize mime-type handling 2025-12-12 23:48:07 -05:00
purian23 44f6ab4878 distro: Reformat workflow newlines 2025-12-12 23:35:37 -05:00
bbedward 5fda6e0f12 clipboard: allow configuration even when disabled 2025-12-12 23:17:55 -05:00
purian23 38068e78c9 distros: PR writeback 2025-12-12 23:02:52 -05:00
purian23 66d22727e9 distros: Enhance build automation 2025-12-12 22:41:51 -05:00
Lucas db2f68e35d nix: fix qt-plugins path (#1005) 2025-12-13 01:34:25 +01:00
Marcus Ramberg 352277ec15 notifications: add ipc call for toggleDoNotDisturb (#1002) 2025-12-12 18:21:00 -05:00
bbedward d6043e64f2 osd: increase shadow buffer
accounts for percentage view
2025-12-12 18:11:31 -05:00
bbedward d3f5b8f32e niri: fix gap reactivity 2025-12-12 16:58:07 -05:00
bbedward 6c3c722674 niri: add warnings on auto-generated files 2025-12-12 16:53:52 -05:00
113 changed files with 12840 additions and 3568 deletions
+383
View File
@@ -0,0 +1,383 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to update (dms, dms-git, or all)"
required: false
default: "all"
force_upload:
description: "Force upload without version check"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
required: false
default: ""
push:
tags:
- "v*"
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract OBS commit, proceeding with update"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
fi
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
PKG="${{ github.event.inputs.package }}"
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🚀 Force upload: all packages"
else
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🚀 Force upload: $PKG"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
update-obs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Determine packages to update
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Update dms-git spec version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
# Update version in spec
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Add changelog entry
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
CHANGELOG_DATE=$(date -R)
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
# Get current version from changelog
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
echo "Current Debian version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
- name: Update dms stable version
if: steps.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE dms spec (stable only)
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update openSUSE spec changelog
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
# Update Debian _service files (both tar_scm and download_url formats)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
done
# Update Debian changelog for dms stable
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R)
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms ($VERSION_NO_V) stable; urgency=medium
* Update to $VERSION stable release
* Bug fixes and improvements
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to $VERSION_NO_V"
fi
- name: Install Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Install OSC
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
if: steps.check-loop.outputs.skip != 'true'
env:
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
fi
if [[ "$PACKAGES" == "all" ]]; then
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changes to commit
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog or spec changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$PKGS" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $PKGS"
fi
- name: Commit packaging changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
+298
View File
@@ -0,0 +1,298 @@
name: Update PPA Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false
default: "dms-git"
force_upload:
description: "Force upload without version check"
required: false
default: "false"
type: choice
options:
- "false"
- "true"
rebuild_release:
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false
default: ""
schedule:
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match changelog format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
upload-ppa:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: |
github.event.inputs.force_upload == 'true' ||
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate_token.outputs.token }}
- name: Check if last commit was automated
id: check-loop
run: |
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "✅ Last commit was not automated, proceeding"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Set up Go
if: steps.check-loop.outputs.skip != 'true'
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache: false
- name: Install build dependencies
if: steps.check-loop.outputs.skip != 'true'
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
if: steps.check-loop.outputs.skip != 'true'
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
if: steps.check-loop.outputs.skip != 'true'
id: packages
run: |
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
PKG="${{ github.event.inputs.package }}"
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
echo "packages=all" >> $GITHUB_OUTPUT
echo "🚀 Force upload: all packages"
else
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "🚀 Force upload: $PKG"
fi
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual package selection should respect change detection
SELECTED_PKG="${{ github.event.inputs.package }}"
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
# Check if manually selected package is in the updated list
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
echo "📦 Manual selection (has updates): $SELECTED_PKG"
else
echo "packages=" >> $GITHUB_OUTPUT
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
fi
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Upload to PPA
if: steps.check-loop.outputs.skip != 'true'
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
if [[ -z "$PACKAGES" ]]; then
echo "No packages selected for upload. Skipping."
exit 0
fi
# Build command arguments
BUILD_ARGS=()
if [[ -n "$REBUILD_RELEASE" ]]; then
BUILD_ARGS+=("$REBUILD_RELEASE")
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi
if [[ "$PACKAGES" == "all" ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..."
if [ -n "$REBUILD_RELEASE" ]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-git to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-greeter to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
else
# Map package to PPA name
case "$PACKAGES" in
dms)
PPA_NAME="dms"
;;
dms-git)
PPA_NAME="dms-git"
;;
dms-greeter)
PPA_NAME="danklinux"
;;
*)
PPA_NAME="$PACKAGES"
;;
esac
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PACKAGES to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
fi
- name: Get changed packages
if: steps.check-loop.outputs.skip != 'true'
id: changed-packages
run: |
# Check if there are any changelog changes to commit
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "📋 No changelog changes to commit"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
# Get list of changed packages for commit message (deduplicate)
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
echo "📋 Changed packages: $CHANGED"
echo "📋 Debug - Changed files:"
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
fi
- name: Commit changelog changes
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
run: |
git config user.name "dms-ci[bot]"
git config user.email "dms-ci[bot]@users.noreply.github.com"
git add distro/ubuntu/*/debian/changelog
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
git pull --rebase origin master
git push
- name: Summary
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-git" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
+192 -136
View File
@@ -4,18 +4,18 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
package: package:
description: 'Package to update (dms, dms-git, or all)' description: "Package to update (dms, dms-git, or all)"
required: false required: false
default: 'all' default: "all"
rebuild_release: rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)' description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
required: false required: false
default: '' default: ""
push: push:
tags: tags:
- 'v*' - "v*"
schedule: schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds - cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs: jobs:
check-updates: check-updates:
@@ -33,78 +33,114 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Check for updates - name: Check for updates
id: check id: check
env:
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
run: | run: |
# Helper function to check dms-git commit
check_dms_git() {
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
return 0 # Update needed
fi
}
# Helper function to check dms stable tag
check_dms_stable() {
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
# Tag push - always update stable package
echo "packages=dms" >> $GITHUB_OUTPUT echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}" VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION (always update)" echo "Triggered by tag: $VERSION (always update)"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..." if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
# Get current commit hash (8 chars to match spec format) else
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract OBS commit, proceeding with update"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
fi fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT # Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}" echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
check_dms_stable && PACKAGES_TO_UPDATE+=("dms")
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
else else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
if check_dms_git; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms" ]]; then
if check_dms_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: $PKG"
fi
else
# Fallback - proceed
echo "packages=all" >> $GITHUB_OUTPUT echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
fi fi
@@ -113,9 +149,7 @@ jobs:
name: Upload to OBS name: Upload to OBS
needs: check-updates needs: check-updates
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: needs.check-updates.outputs.has_updates == 'true'
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps: steps:
- name: Checkout - name: Checkout
@@ -135,8 +169,14 @@ jobs:
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package" echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Use filtered packages from check-updates when package="all" and no rebuild requested
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
else
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}" echo "Manual trigger: ${{ github.event.inputs.package }}"
fi
else else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi fi
@@ -144,66 +184,42 @@ jobs:
- name: Update dms-git spec version - name: Update dms-git spec version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all' if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: | run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD) COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD) COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2") BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}" NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating dms-git.spec to version: $NEW_VERSION" echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
# Update version in spec
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
# Add changelog entry # Single changelog entry (git snapshots don't need history)
DATE_STR=$(date "+%a %b %d %Y") DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)" LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec {
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
} > distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version - name: Update Debian dms-git changelog version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all' if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: | run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD) COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD) COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2") BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}" NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION" echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
# Single changelog entry (git snapshots don't need history)
CHANGELOG_DATE=$(date -R) CHANGELOG_DATE=$(date -R)
{
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog" echo "dms-git ($NEW_VERSION) nightly; urgency=medium"
echo ""
# Get current version from changelog echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/') echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
echo "Current Debian version: $CURRENT_VERSION" } > "distro/debian/dms-git/debian/changelog"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
- name: Update dms stable version - name: Update dms stable version
if: steps.packages.outputs.version != '' if: steps.packages.outputs.version != ''
@@ -212,13 +228,17 @@ jobs:
VERSION_NO_V="${VERSION#v}" VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V" echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE dms spec (stable only)
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
# Update openSUSE spec changelog # Single changelog entry (full history on OBS website)
DATE_STR=$(date "+%a %b %d %Y") DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements" LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec {
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
echo "- Update to stable $VERSION release"
} > distro/opensuse/dms.spec
# Update Debian _service files (both tar_scm and download_url formats) # Update Debian _service files (both tar_scm and download_url formats)
for service in distro/debian/*/_service; do for service in distro/debian/*/_service; do
@@ -232,31 +252,23 @@ jobs:
fi fi
done done
# Update Debian changelog for dms stable # Update Debian changelog for dms stable (single entry, history on OBS website)
if [[ -f "distro/debian/dms/debian/changelog" ]]; then if [[ -f "distro/debian/dms/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R) CHANGELOG_DATE=$(date -R)
TEMP_CHANGELOG=$(mktemp) {
echo "dms ($VERSION_NO_V) stable; urgency=medium"
cat > "$TEMP_CHANGELOG" << EOF echo ""
dms ($VERSION_NO_V) stable; urgency=medium echo " * Update to $VERSION stable release"
echo ""
* Update to $VERSION stable release echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
* Bug fixes and improvements } > "distro/debian/dms/debian/changelog"
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to $VERSION_NO_V" echo "✓ Updated Debian changelog to $VERSION_NO_V"
fi fi
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.24' go-version: "1.24"
- name: Install OSC - name: Install OSC
run: | run: |
@@ -276,32 +288,76 @@ jobs:
- name: Upload to OBS - name: Upload to OBS
env: env:
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }} REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: | run: |
PACKAGES="${{ steps.packages.outputs.packages }}" PACKAGES="${{ steps.packages.outputs.packages }}"
MESSAGE="Automated update from GitHub Actions"
if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!"
exit 0
fi
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}" MESSAGE="Update to ${{ steps.packages.outputs.version }}"
fi fi
if [[ "$PACKAGES" == "all" ]]; then # PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
bash distro/scripts/obs-upload.sh dms "$MESSAGE" # Loop through each package and upload
for PKG in $PACKAGES; do
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PKG to OBS..."
if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ "$PKG" == "dms-git" ]]; then
bash distro/scripts/obs-upload.sh dms-git "Automated git update" bash distro/scripts/obs-upload.sh dms-git "Automated git update"
else else
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE" bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
fi fi
done
- name: Summary - name: Summary
if: always()
run: | run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ -z "$PACKAGES" ]]; then
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
else
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do
case "$PKG" in
dms)
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
;;
dms-git)
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi fi
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
fi fi
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
+174 -89
View File
@@ -4,15 +4,15 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
package: package:
description: 'Package to upload (dms, dms-git, dms-greeter, or all)' description: "Package to upload (dms, dms-git, dms-greeter, or all)"
required: false required: false
default: 'dms-git' default: "dms-git"
rebuild_release: rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)' description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
required: false required: false
default: '' default: ""
schedule: schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds - cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs: jobs:
check-updates: check-updates:
@@ -32,41 +32,112 @@ jobs:
- name: Check for updates - name: Check for updates
id: check id: check
run: | run: |
# Helper function to check dms-git commit
check_dms_git() {
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
return 0 # Update needed
fi
}
# Helper function to check stable package tag
check_stable_package() {
local PKG="$1"
local PPA_NAME="$2"
local LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "v\?\([^"]*\)".*/\1/' || echo "")
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.event_name }}" == "schedule" ]]; then if [[ "${{ github.event_name }}" == "schedule" ]]; then
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..." if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
# Get current commit hash (8 chars to match changelog format) else
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
fi fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT # Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}" echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
elif [[ "$PKG" == "all" ]]; then
# Check each package and build list of those needing updates
PACKAGES_TO_UPDATE=()
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
else else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "✓ All packages up to date"
fi
elif [[ "$PKG" == "dms-git" ]]; then
if check_dms_git; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms" ]]; then
if check_stable_package "dms" "dms"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_stable_package "dms-greeter" "danklinux"; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: $PKG"
fi
else
# Fallback
echo "packages=dms-git" >> $GITHUB_OUTPUT echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
fi fi
@@ -75,9 +146,7 @@ jobs:
name: Upload to PPA name: Upload to PPA
needs: check-updates needs: check-updates
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: needs.check-updates.outputs.has_updates == 'true'
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps: steps:
- name: Checkout - name: Checkout
@@ -88,7 +157,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.24' go-version: "1.24"
cache: false cache: false
- name: Install build dependencies - name: Install build dependencies
@@ -114,81 +183,97 @@ jobs:
- name: Determine packages to upload - name: Determine packages to upload
id: packages id: packages
run: | run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then # Use packages determined by check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "Triggered by schedule: uploading git package" echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi fi
- name: Upload to PPA - name: Upload to PPA
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: | run: |
PACKAGES="${{ steps.packages.outputs.packages }}" PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
# Export to ensure it's available to subprocesses if [[ -z "$PACKAGES" ]]; then
if [ -n "$REBUILD_RELEASE" ]; then echo "✓ No packages need uploading. All up to date!"
exit 0
fi
# Export REBUILD_RELEASE so ppa-build.sh can use it
if [[ -n "$REBUILD_RELEASE" ]]; then
export REBUILD_RELEASE export REBUILD_RELEASE
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE" echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
fi fi
if [[ "$PACKAGES" == "all" ]]; then # PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload
for PKG in $PACKAGES; do
# Map package to PPA name
case "$PKG" in
dms)
PPA_NAME="dms"
;;
dms-git)
PPA_NAME="dms-git"
;;
dms-greeter)
PPA_NAME="danklinux"
;;
*)
echo "⚠️ Unknown package: $PKG, skipping"
continue
;;
esac
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..." echo "Uploading $PKG to PPA $PPA_NAME..."
if [ -n "$REBUILD_RELEASE" ]; then if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE" echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
done
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-git to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-greeter to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
else
PPA_NAME="$PACKAGES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PACKAGES to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
fi
- name: Summary - name: Summary
if: always()
run: | run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
PACKAGES="${{ steps.packages.outputs.packages }}" PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY if [[ -z "$PACKAGES" ]]; then
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms" ]]; then echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY else
elif [[ "$PACKAGES" == "dms-git" ]]; then echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY for PKG in $PACKAGES; do
case "$PKG" in
dms)
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-git)
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
;;
esac
done
echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
fi
+19
View File
@@ -0,0 +1,19 @@
name: Update stable branch
on:
push:
tags:
- "v*"
jobs:
update-stable:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push to stable branch
run: git push origin HEAD:refs/heads/stable --force
-6
View File
@@ -104,12 +104,6 @@ go.work.sum
bin/ bin/
# Extracted source trees in Ubuntu package directories
distro/ubuntu/*/dms-git-repo/
distro/ubuntu/*/DankMaterialShell-*/
distro/ubuntu/danklinux/*/dsearch-*/
distro/ubuntu/danklinux/*/dgop-*/
# direnv # direnv
.envrc .envrc
.direnv/ .direnv/
+10 -1
View File
@@ -2,6 +2,15 @@ repos:
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.6.2 rev: v2.6.2
hooks: hooks:
- id: golangci-lint-full
- id: golangci-lint-fmt - id: golangci-lint-fmt
require_serial: true
- id: golangci-lint-full
- id: golangci-lint-config-verify - id: golangci-lint-config-verify
- repo: local
hooks:
- id: go-test
name: go test
entry: go test ./...
language: system
pass_filenames: false
types: [go]
+20 -44
View File
@@ -15,6 +15,7 @@ import (
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff" _ "golang.org/x/image/tiff"
"hash/fnv"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
@@ -39,6 +40,7 @@ type Entry struct {
Size int Size int
Timestamp time.Time Timestamp time.Time
IsImage bool IsImage bool
Hash uint64
} }
func Store(data []byte, mimeType string) error { func Store(data []byte, mimeType string) error {
@@ -70,6 +72,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
Size: len(data), Size: len(data),
Timestamp: time.Now(), Timestamp: time.Now(),
IsImage: IsImageMimeType(mimeType), IsImage: IsImageMimeType(mimeType),
Hash: computeHash(data),
} }
switch { switch {
@@ -85,7 +88,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
return err return err
} }
if err := deduplicateInTx(b, data); err != nil { if err := deduplicateInTx(b, entry.Hash); err != nil {
return err return err
} }
@@ -126,19 +129,16 @@ func getDBPath() (string, error) {
return filepath.Join(dbDir, "db"), nil return filepath.Join(dbDir, "db"), nil
} }
func deduplicateInTx(b *bolt.Bucket, data []byte) error { func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
c := b.Cursor() c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() { for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v) if extractHash(v) != hash {
if err != nil {
continue continue
} }
if bytes.Equal(entry.Data, data) {
if err := b.Delete(k); err != nil { if err := b.Delete(k); err != nil {
return err return err
} }
} }
}
return nil return nil
} }
@@ -174,54 +174,30 @@ func encodeEntry(e Entry) ([]byte, error) {
} else { } else {
buf.WriteByte(0) buf.WriteByte(0)
} }
binary.Write(buf, binary.BigEndian, e.Hash)
return buf.Bytes(), nil return buf.Bytes(), nil
} }
func decodeEntry(data []byte) (Entry, error) {
buf := bytes.NewReader(data)
var e Entry
binary.Read(buf, binary.BigEndian, &e.ID)
var dataLen uint32
binary.Read(buf, binary.BigEndian, &dataLen)
e.Data = make([]byte, dataLen)
buf.Read(e.Data)
var mimeLen uint32
binary.Read(buf, binary.BigEndian, &mimeLen)
mimeBytes := make([]byte, mimeLen)
buf.Read(mimeBytes)
e.MimeType = string(mimeBytes)
var prevLen uint32
binary.Read(buf, binary.BigEndian, &prevLen)
prevBytes := make([]byte, prevLen)
buf.Read(prevBytes)
e.Preview = string(prevBytes)
var size int32
binary.Read(buf, binary.BigEndian, &size)
e.Size = int(size)
var timestamp int64
binary.Read(buf, binary.BigEndian, &timestamp)
e.Timestamp = time.Unix(timestamp, 0)
var isImage byte
binary.Read(buf, binary.BigEndian, &isImage)
e.IsImage = isImage == 1
return e, nil
}
func itob(v uint64) []byte { func itob(v uint64) []byte {
b := make([]byte, 8) b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v) binary.BigEndian.PutUint64(b, v)
return b return b
} }
func computeHash(data []byte) uint64 {
h := fnv.New64a()
h.Write(data)
return h.Sum64()
}
func extractHash(data []byte) uint64 {
if len(data) < 8 {
return 0
}
return binary.BigEndian.Uint64(data[len(data)-8:])
}
func textPreview(data []byte) string { func textPreview(data []byte) string {
text := string(data) text := string(data)
text = strings.TrimSpace(text) text = strings.TrimSpace(text)
+1 -1
View File
@@ -276,4 +276,4 @@ bind = CTRL, Print, exec, dms screenshot full
bind = ALT, Print, exec, dms screenshot window bind = ALT, Print, exec, dms screenshot window
# === System Controls === # === System Controls ===
bind = $mod SHIFT, P, dpms, off bind = $mod SHIFT, P, dpms, toggle
@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
recent-windows { recent-windows {
highlight { highlight {
corner-radius 12 corner-radius 12
@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
binds { binds {
// === System & Overview === // === System & Overview ===
Mod+D repeat=false { toggle-overview; } Mod+D repeat=false { toggle-overview; }
@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout { layout {
background-color "transparent" background-color "transparent"
@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout { layout {
gaps 4 gaps 4
+49
View File
@@ -18,15 +18,64 @@ gestures {
input { input {
keyboard { keyboard {
xkb { xkb {
// You can set rules, model, layout, variant and options.
// For more information, see xkeyboard-config(7).
// For example:
// layout "us,ru"
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
// If this section is empty, niri will fetch xkb settings
// from org.freedesktop.locale1. You can control these using
// localectl set-x11-keymap.
} }
// Enable numlock on startup, omitting this setting disables it.
numlock numlock
} }
// Next sections include libinput settings.
// Omitting settings disables them, or leaves them at their default values.
// All commented-out settings here are examples, not defaults.
touchpad { touchpad {
// off
tap
// dwt
// dwtp
// drag false
// drag-lock
natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "two-finger"
// disabled-on-external-mouse
} }
mouse { mouse {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
} }
trackpoint { trackpoint {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "on-button-down"
// scroll-button 273
// scroll-button-lock
// middle-emulation
} }
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
// focus-follows-mouse max-scroll-amount="0%"
} }
// You can configure outputs by their name, which you can find // You can configure outputs by their name, which you can find
// by running `niri msg outputs` while inside a niri instance. // by running `niri msg outputs` while inside a niri instance.
+9 -1
View File
@@ -461,9 +461,16 @@ func (n *NiriProvider) getBindSortPriority(action string) int {
} }
} }
const dmsWarningHeader = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string { func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
if len(binds) == 0 { if len(binds) == 0 {
return "binds {}\n" return dmsWarningHeader + "binds {}\n"
} }
var regularBinds, recentWindowsBinds []*overrideBind var regularBinds, recentWindowsBinds []*overrideBind
@@ -490,6 +497,7 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
var sb strings.Builder var sb strings.Builder
sb.WriteString(dmsWarningHeader)
sb.WriteString("binds {\n") sb.WriteString("binds {\n")
for _, bind := range regularBinds { for _, bind := range regularBinds {
n.writeBindNode(&sb, bind, " ") n.writeBindNode(&sb, bind, " ")
+20 -13
View File
@@ -6,6 +6,13 @@ import (
"testing" "testing"
) )
const testHeader = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`
func TestNiriProviderName(t *testing.T) { func TestNiriProviderName(t *testing.T) {
provider := NewNiriProvider("") provider := NewNiriProvider("")
if provider.Name() != "niri" { if provider.Name() != "niri" {
@@ -197,7 +204,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
{ {
name: "empty binds", name: "empty binds",
binds: map[string]*overrideBind{}, binds: map[string]*overrideBind{},
expected: "binds {}\n", expected: testHeader + "binds {}\n",
}, },
{ {
name: "simple spawn bind", name: "simple spawn bind",
@@ -208,7 +215,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Open Terminal", Description: "Open Terminal",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; } Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
} }
`, `,
@@ -222,7 +229,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Application Launcher", Description: "Application Launcher",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
} }
`, `,
@@ -236,7 +243,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Options: map[string]any{"allow-when-locked": true}, Options: map[string]any{"allow-when-locked": true},
}, },
}, },
expected: `binds { expected: testHeader + `binds {
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; } XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
} }
`, `,
@@ -250,7 +257,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Close Window", Description: "Close Window",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
Mod+Q hotkey-overlay-title="Close Window" { close-window; } Mod+Q hotkey-overlay-title="Close Window" { close-window; }
} }
`, `,
@@ -263,7 +270,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Action: "next-window", Action: "next-window",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
} }
recent-windows { recent-windows {
@@ -415,7 +422,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Focus Workspace 1", Description: "Focus Workspace 1",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; } Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
} }
`, `,
@@ -429,7 +436,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Focus Workspace 10", Description: "Focus Workspace 10",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; } Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
} }
`, `,
@@ -443,7 +450,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Adjust Column Width -10%", Description: "Adjust Column Width -10%",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; } Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
} }
`, `,
@@ -457,7 +464,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Adjust Column Width +10%", Description: "Adjust Column Width +10%",
}, },
}, },
expected: `binds { expected: testHeader + `binds {
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; } Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
} }
`, `,
@@ -486,7 +493,7 @@ func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
} }
content := provider.generateBindsContent(binds) content := provider.generateBindsContent(binds)
expected := `binds { expected := testHeader + `binds {
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; } Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
} }
` `
@@ -507,7 +514,7 @@ func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
} }
content := provider.generateBindsContent(binds) content := provider.generateBindsContent(binds)
expected := `binds { expected := testHeader + `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; } XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
} }
` `
@@ -528,7 +535,7 @@ func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
} }
content := provider.generateBindsContent(binds) content := provider.generateBindsContent(binds)
expected := `binds { expected := testHeader + `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; } XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
} }
` `
@@ -238,7 +238,7 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy == nil { if proxy == nil || proxy.IsZombie() {
head := &ZwlrOutputHeadV1{} head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context()) head.SetContext(i.Context())
head.SetID(objectID) head.SetID(objectID)
@@ -723,7 +723,7 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy == nil { if proxy == nil || proxy.IsZombie() {
mode := &ZwlrOutputModeV1{} mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context()) mode.SetContext(i.Context())
mode.SetID(objectID) mode.SetID(objectID)
@@ -761,8 +761,8 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0 l := 0
objectID := client.Uint32(data[l : l+4]) objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID) proxy := i.Context().GetProxy(objectID)
if proxy == nil { if proxy == nil || proxy.IsZombie() {
// Mode not yet registered, create it // Mode not yet registered or zombie, create fresh
mode := &ZwlrOutputModeV1{} mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context()) mode.SetContext(i.Context())
mode.SetID(objectID) mode.SetID(objectID)
+175 -95
View File
@@ -18,6 +18,7 @@ import (
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff" _ "golang.org/x/image/tiff"
"hash/fnv"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
@@ -69,6 +70,10 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
} }
m.db = db m.db = db
if err := m.migrateHashes(); err != nil {
log.Errorf("Failed to migrate hashes: %v", err)
}
if config.ClearAtStartup { if config.ClearAtStartup {
if err := m.clearHistoryInternal(); err != nil { if err := m.clearHistoryInternal(); err != nil {
log.Errorf("Failed to clear history at startup: %v", err) log.Errorf("Failed to clear history at startup: %v", err)
@@ -244,7 +249,30 @@ func (m *Manager) setupDataDeviceSync() {
m.mimeTypes = mimes m.mimeTypes = mimes
go m.storeCurrentClipboard() if len(mimes) == 0 {
return
}
preferredMime := m.selectMimeType(mimes)
if preferredMime == "" {
return
}
typedOffer := offer.(*ext_data_control.ExtDataControlOfferV1)
r, w, err := os.Pipe()
if err != nil {
return
}
if err := typedOffer.Receive(preferredMime, int(w.Fd())); err != nil {
r.Close()
w.Close()
return
}
w.Close()
go m.readAndStore(r, preferredMime)
}) })
if err := dataMgr.GetDataDeviceWithProxy(dataDevice, m.seat); err != nil { if err := dataMgr.GetDataDeviceWithProxy(dataDevice, m.seat); err != nil {
@@ -262,60 +290,55 @@ func (m *Manager) setupDataDeviceSync() {
log.Info("Data device setup complete") log.Info("Data device setup complete")
} }
func (m *Manager) storeCurrentClipboard() { func (m *Manager) readAndStore(r *os.File, mimeType string) {
if m.currentOffer == nil { defer r.Close()
return
}
cfg := m.getConfig() cfg := m.getConfig()
offer := m.currentOffer.(*ext_data_control.ExtDataControlOfferV1) done := make(chan []byte, 1)
go func() {
data, _ := io.ReadAll(r)
done <- data
}()
if len(m.mimeTypes) == 0 { var data []byte
select {
case data = <-done:
case <-time.After(500 * time.Millisecond):
return return
} }
allData := make(map[string][]byte)
var orderedMimes []string
for _, mime := range m.mimeTypes {
data, err := m.receiveData(offer, mime)
if err != nil {
continue
}
if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize { if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize {
continue
}
allData[mime] = data
orderedMimes = append(orderedMimes, mime)
}
if len(allData) == 0 {
return return
} }
preferredMime := m.selectMimeType(orderedMimes)
if preferredMime == "" {
preferredMime = orderedMimes[0]
}
data := allData[preferredMime]
if len(bytes.TrimSpace(data)) == 0 { if len(bytes.TrimSpace(data)) == 0 {
return return
} }
if !cfg.DisableHistory && m.db != nil { if !cfg.DisableHistory && m.db != nil {
m.storeClipboardEntry(data, mimeType)
}
if !cfg.DisablePersist {
m.persistClipboard([]string{mimeType}, map[string][]byte{mimeType: data})
}
m.updateState()
m.notifySubscribers()
}
func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
entry := Entry{ entry := Entry{
Data: data, Data: data,
MimeType: preferredMime, MimeType: mimeType,
Size: len(data), Size: len(data),
Timestamp: time.Now(), Timestamp: time.Now(),
IsImage: m.isImageMimeType(preferredMime), IsImage: m.isImageMimeType(mimeType),
} }
switch { switch {
case entry.IsImage: case entry.IsImage:
entry.Preview = m.imagePreview(data, preferredMime) entry.Preview = m.imagePreview(data, mimeType)
default: default:
entry.Preview = m.textPreview(data) entry.Preview = m.textPreview(data)
} }
@@ -323,14 +346,6 @@ func (m *Manager) storeCurrentClipboard() {
if err := m.storeEntry(entry); err != nil { if err := m.storeEntry(entry); err != nil {
log.Errorf("Failed to store clipboard entry: %v", err) log.Errorf("Failed to store clipboard entry: %v", err)
} }
}
if !cfg.DisablePersist {
m.persistClipboard(orderedMimes, allData)
}
m.updateState()
m.notifySubscribers()
} }
func (m *Manager) persistClipboard(mimeTypes []string, data map[string][]byte) { func (m *Manager) persistClipboard(mimeTypes []string, data map[string][]byte) {
@@ -415,14 +430,34 @@ func (m *Manager) takePersistOwnership() {
m.ownerLock.Unlock() m.ownerLock.Unlock()
} }
func (m *Manager) releaseOwnership() {
m.ownerLock.Lock()
m.isOwner = false
m.ownerLock.Unlock()
m.persistMutex.Lock()
m.persistData = nil
m.persistMimeTypes = nil
m.persistMutex.Unlock()
if m.currentSource != nil {
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
source.Destroy()
m.currentSource = nil
}
}
func (m *Manager) storeEntry(entry Entry) error { func (m *Manager) storeEntry(entry Entry) error {
if m.db == nil { if m.db == nil {
return fmt.Errorf("database not available") return fmt.Errorf("database not available")
} }
entry.Hash = computeHash(entry.Data)
return m.db.Update(func(tx *bolt.Tx) error { return m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard")) b := tx.Bucket([]byte("clipboard"))
if err := m.deduplicateInTx(b, entry.Data); err != nil { if err := m.deduplicateInTx(b, entry.Hash); err != nil {
return err return err
} }
@@ -446,19 +481,16 @@ func (m *Manager) storeEntry(entry Entry) error {
}) })
} }
func (m *Manager) deduplicateInTx(b *bolt.Bucket, data []byte) error { func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
c := b.Cursor() c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() { for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v) if extractHash(v) != hash {
if err != nil {
continue continue
} }
if bytes.Equal(entry.Data, data) {
if err := b.Delete(k); err != nil { if err := b.Delete(k); err != nil {
return err return err
} }
} }
}
return nil return nil
} }
@@ -494,6 +526,7 @@ func encodeEntry(e Entry) ([]byte, error) {
} else { } else {
buf.WriteByte(0) buf.WriteByte(0)
} }
binary.Write(buf, binary.BigEndian, e.Hash)
return buf.Bytes(), nil return buf.Bytes(), nil
} }
@@ -533,6 +566,10 @@ func decodeEntry(data []byte) (Entry, error) {
binary.Read(buf, binary.BigEndian, &isImage) binary.Read(buf, binary.BigEndian, &isImage)
e.IsImage = isImage == 1 e.IsImage = isImage == 1
if buf.Len() >= 8 {
binary.Read(buf, binary.BigEndian, &e.Hash)
}
return e, nil return e, nil
} }
@@ -542,6 +579,19 @@ func itob(v uint64) []byte {
return b return b
} }
func computeHash(data []byte) uint64 {
h := fnv.New64a()
h.Write(data)
return h.Sum64()
}
func extractHash(data []byte) uint64 {
if len(data) < 8 {
return 0
}
return binary.BigEndian.Uint64(data[len(data)-8:])
}
func (m *Manager) selectMimeType(mimes []string) string { func (m *Manager) selectMimeType(mimes []string) string {
preferredTypes := []string{ preferredTypes := []string{
"text/plain;charset=utf-8", "text/plain;charset=utf-8",
@@ -551,8 +601,9 @@ func (m *Manager) selectMimeType(mimes []string) string {
"TEXT", "TEXT",
"image/png", "image/png",
"image/jpeg", "image/jpeg",
"image/bmp",
"image/gif", "image/gif",
"image/bmp",
"image/tiff",
} }
for _, pref := range preferredTypes { for _, pref := range preferredTypes {
@@ -563,6 +614,10 @@ func (m *Manager) selectMimeType(mimes []string) string {
} }
} }
if len(mimes) > 0 {
return mimes[0]
}
return "" return ""
} }
@@ -570,37 +625,6 @@ func (m *Manager) isImageMimeType(mime string) bool {
return strings.HasPrefix(mime, "image/") return strings.HasPrefix(mime, "image/")
} }
func (m *Manager) receiveData(offer *ext_data_control.ExtDataControlOfferV1, mimeType string) ([]byte, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
defer r.Close()
if err := offer.Receive(mimeType, int(w.Fd())); err != nil {
w.Close()
return nil, err
}
w.Close()
type result struct {
data []byte
err error
}
done := make(chan result, 1)
go func() {
data, err := io.ReadAll(r)
done <- result{data, err}
}()
select {
case res := <-done:
return res.data, res.err
case <-time.After(100 * time.Millisecond):
return nil, fmt.Errorf("timeout reading clipboard data")
}
}
func (m *Manager) textPreview(data []byte) string { func (m *Manager) textPreview(data []byte) string {
text := string(data) text := string(data)
text = strings.TrimSpace(text) text = strings.TrimSpace(text)
@@ -1052,6 +1076,79 @@ func (m *Manager) clearOldEntries(days int) error {
}) })
} }
func (m *Manager) migrateHashes() error {
if m.db == nil {
return nil
}
var needsMigration bool
if err := m.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if extractHash(v) == 0 {
needsMigration = true
return nil
}
}
return nil
}); err != nil {
return err
}
if !needsMigration {
return nil
}
log.Info("Migrating clipboard entries to add hashes...")
return m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
if b == nil {
return nil
}
var updates []struct {
key []byte
entry Entry
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
entry, err := decodeEntry(v)
if err != nil {
continue
}
if entry.Hash != 0 {
continue
}
entry.Hash = computeHash(entry.Data)
keyCopy := make([]byte, len(k))
copy(keyCopy, k)
updates = append(updates, struct {
key []byte
entry Entry
}{keyCopy, entry})
}
for _, u := range updates {
encoded, err := encodeEntry(u.entry)
if err != nil {
continue
}
if err := b.Put(u.key, encoded); err != nil {
return err
}
}
log.Infof("Migrated %d clipboard entries", len(updates))
return nil
})
}
func (m *Manager) Search(params SearchParams) SearchResult { func (m *Manager) Search(params SearchParams) SearchResult {
if m.db == nil { if m.db == nil {
return SearchResult{} return SearchResult{}
@@ -1224,23 +1321,6 @@ func (m *Manager) applyConfigChange(newCfg Config) {
m.notifySubscribers() m.notifySubscribers()
} }
func (m *Manager) releaseOwnership() {
m.ownerLock.Lock()
m.isOwner = false
m.ownerLock.Unlock()
m.persistMutex.Lock()
m.persistData = nil
m.persistMimeTypes = nil
m.persistMutex.Unlock()
if m.currentSource != nil {
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
source.Destroy()
m.currentSource = nil
}
}
func (m *Manager) StoreData(data []byte, mimeType string) error { func (m *Manager) StoreData(data []byte, mimeType string) error {
cfg := m.getConfig() cfg := m.getConfig()
@@ -409,8 +409,10 @@ func TestSelectMimeType(t *testing.T) {
}{ }{
{[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"}, {[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"},
{[]string{"text/html", "text/plain"}, "text/plain"}, {[]string{"text/html", "text/plain"}, "text/plain"},
{[]string{"application/json", "image/png"}, "image/png"}, {[]string{"text/html", "image/png"}, "image/png"},
{[]string{"application/json", "application/xml"}, ""}, {[]string{"image/png", "image/jpeg"}, "image/png"},
{[]string{"image/png"}, "image/png"},
{[]string{"application/octet-stream"}, "application/octet-stream"},
{[]string{}, ""}, {[]string{}, ""},
} }
+2
View File
@@ -103,6 +103,7 @@ type Entry struct {
Size int `json:"size"` Size int `json:"size"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
IsImage bool `json:"isImage"` IsImage bool `json:"isImage"`
Hash uint64 `json:"hash,omitempty"`
} }
type State struct { type State struct {
@@ -140,6 +141,7 @@ type Manager struct {
isOwner bool isOwner bool
ownerLock sync.Mutex ownerLock sync.Mutex
initialized bool initialized bool
alive bool alive bool
@@ -11,4 +11,9 @@ const (
dbusPortalSettingsInterface = "org.freedesktop.portal.Settings" dbusPortalSettingsInterface = "org.freedesktop.portal.Settings"
dbusPropsInterface = "org.freedesktop.DBus.Properties" dbusPropsInterface = "org.freedesktop.DBus.Properties"
dbusScreensaverName = "org.freedesktop.ScreenSaver"
dbusScreensaverPath = "/ScreenSaver"
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
) )
@@ -24,6 +24,7 @@ func NewManager() (*Manager, error) {
state: &FreedeskState{ state: &FreedeskState{
Accounts: AccountsState{}, Accounts: AccountsState{},
Settings: SettingsState{}, Settings: SettingsState{},
Screensaver: ScreensaverState{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
systemConn: systemConn, systemConn: systemConn,
@@ -33,6 +34,7 @@ func NewManager() (*Manager, error) {
m.initializeAccounts() m.initializeAccounts()
m.initializeSettings() m.initializeSettings()
m.initializeScreensaver()
return m, nil return m, nil
} }
@@ -0,0 +1,250 @@
package freedesktop
import (
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
)
type screensaverHandler struct {
manager *Manager
}
func (m *Manager) initializeScreensaver() error {
if m.sessionConn == nil {
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
if err != nil {
log.Warnf("Failed to request screensaver name: %v", err)
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name already owned by another process")
m.stateMutex.Lock()
m.state.Screensaver.Available = false
m.stateMutex.Unlock()
return nil
}
handler := &screensaverHandler{manager: m}
if err := m.sessionConn.Export(handler, dbusScreensaverPath, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath, err)
return nil
}
if err := m.sessionConn.Export(handler, dbusScreensaverPath2, dbusScreensaverInterface); err != nil {
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath2, err)
return nil
}
introNode := &introspect.Node{
Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
}
introNode2 := &introspect.Node{
Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
}
go m.watchPeerDisconnects()
m.stateMutex.Lock()
m.state.Screensaver.Available = true
m.state.Screensaver.Inhibited = false
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
m.stateMutex.Unlock()
log.Info("Screensaver inhibit listener initialized")
return nil
}
func (h *screensaverHandler) Inhibit(sender dbus.Sender, appName, reason string) (uint32, *dbus.Error) {
if appName == "" {
return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"application name required"})
}
if reason == "" {
return 0, dbus.NewError("org.freedesktop.DBus.Error.InvalidArgs", []any{"reason required"})
}
if strings.Contains(strings.ToLower(reason), "audio") && !strings.Contains(strings.ToLower(reason), "video") {
log.Debugf("Ignoring audio-only inhibit from %s: %s", appName, reason)
return 0, nil
}
if idx := strings.LastIndex(appName, "/"); idx != -1 && idx < len(appName)-1 {
appName = appName[idx+1:]
}
appName = filepath.Base(appName)
cookie := atomic.AddUint32(&h.manager.screensaverCookieCounter, 1)
inhibitor := ScreensaverInhibitor{
Cookie: cookie,
AppName: appName,
Reason: reason,
Peer: string(sender),
StartTime: time.Now().Unix(),
}
h.manager.stateMutex.Lock()
h.manager.state.Screensaver.Inhibitors = append(h.manager.state.Screensaver.Inhibitors, inhibitor)
h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0
h.manager.stateMutex.Unlock()
log.Infof("Screensaver inhibited by %s (%s): %s -> cookie %08X", appName, sender, reason, cookie)
h.manager.NotifyScreensaverSubscribers()
return cookie, nil
}
func (h *screensaverHandler) UnInhibit(sender dbus.Sender, cookie uint32) *dbus.Error {
h.manager.stateMutex.Lock()
defer h.manager.stateMutex.Unlock()
found := false
inhibitors := h.manager.state.Screensaver.Inhibitors
for i, inh := range inhibitors {
if inh.Cookie != cookie {
continue
}
log.Infof("Screensaver uninhibited by %s (%s) cookie %08X", inh.AppName, sender, cookie)
h.manager.state.Screensaver.Inhibitors = append(inhibitors[:i], inhibitors[i+1:]...)
found = true
break
}
if !found {
log.Debugf("UnInhibit: no match for cookie %08X", cookie)
return nil
}
h.manager.state.Screensaver.Inhibited = len(h.manager.state.Screensaver.Inhibitors) > 0
go h.manager.NotifyScreensaverSubscribers()
return nil
}
func (m *Manager) watchPeerDisconnects() {
if m.sessionConn == nil {
return
}
if err := m.sessionConn.AddMatchSignal(
dbus.WithMatchInterface("org.freedesktop.DBus"),
dbus.WithMatchMember("NameOwnerChanged"),
); err != nil {
log.Warnf("Failed to watch peer disconnects: %v", err)
return
}
signals := make(chan *dbus.Signal, 64)
m.sessionConn.Signal(signals)
for sig := range signals {
if sig.Name != "org.freedesktop.DBus.NameOwnerChanged" {
continue
}
if len(sig.Body) < 3 {
continue
}
name, ok1 := sig.Body[0].(string)
newOwner, ok2 := sig.Body[2].(string)
if !ok1 || !ok2 {
continue
}
if newOwner != "" {
continue
}
m.removeInhibitorsByPeer(name)
}
}
func (m *Manager) removeInhibitorsByPeer(peer string) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
var remaining []ScreensaverInhibitor
var removed []ScreensaverInhibitor
for _, inh := range m.state.Screensaver.Inhibitors {
if inh.Peer == peer {
removed = append(removed, inh)
continue
}
remaining = append(remaining, inh)
}
if len(removed) == 0 {
return
}
for _, inh := range removed {
log.Infof("Screensaver: peer %s died, removing inhibitor from %s (cookie %08X)", peer, inh.AppName, inh.Cookie)
}
m.state.Screensaver.Inhibitors = remaining
m.state.Screensaver.Inhibited = len(remaining) > 0
go m.NotifyScreensaverSubscribers()
}
func (m *Manager) GetScreensaverState() ScreensaverState {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
return m.state.Screensaver
}
func (m *Manager) SubscribeScreensaver(id string) chan ScreensaverState {
ch := make(chan ScreensaverState, 64)
m.screensaverSubscribers.Store(id, ch)
return ch
}
func (m *Manager) UnsubscribeScreensaver(id string) {
if val, ok := m.screensaverSubscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) NotifyScreensaverSubscribers() {
state := m.GetScreensaverState()
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
select {
case ch <- state:
default:
}
return true
})
}
+17
View File
@@ -29,9 +29,24 @@ type SettingsState struct {
ColorScheme uint32 `json:"colorScheme"` ColorScheme uint32 `json:"colorScheme"`
} }
type ScreensaverInhibitor struct {
Cookie uint32 `json:"cookie"`
AppName string `json:"appName"`
Reason string `json:"reason"`
Peer string `json:"peer"`
StartTime int64 `json:"startTime"`
}
type ScreensaverState struct {
Available bool `json:"available"`
Inhibited bool `json:"inhibited"`
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
}
type FreedeskState struct { type FreedeskState struct {
Accounts AccountsState `json:"accounts"` Accounts AccountsState `json:"accounts"`
Settings SettingsState `json:"settings"` Settings SettingsState `json:"settings"`
Screensaver ScreensaverState `json:"screensaver"`
} }
type Manager struct { type Manager struct {
@@ -43,4 +58,6 @@ type Manager struct {
settingsObj dbus.BusObject settingsObj dbus.BusObject
currentUID uint64 currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState] subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32
} }
@@ -880,22 +880,18 @@ func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImp
} }
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) { func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
args := []string{"connection", "import", "type", "openvpn", "file", filePath} vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
cmd := exec.Command("nmcli", args...)
output, err := cmd.CombinedOutput()
if err != nil { var output []byte
outputStr := string(output) var err error
if strings.Contains(outputStr, "vpnc") || strings.Contains(outputStr, "unknown connection type") { for _, vpnType := range vpnTypes {
for _, vpnType := range []string{"vpnc", "pptp", "l2tp", "openconnect", "strongswan", "wireguard"} { args := []string{"connection", "import", "type", vpnType, "file", filePath}
args = []string{"connection", "import", "type", vpnType, "file", filePath} cmd := exec.Command("nmcli", args...)
cmd = exec.Command("nmcli", args...)
output, err = cmd.CombinedOutput() output, err = cmd.CombinedOutput()
if err == nil { if err == nil {
break break
} }
} }
}
if err != nil { if err != nil {
return &VPNImportResult{ return &VPNImportResult{
@@ -903,7 +899,6 @@ func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string)
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))), Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
}, nil }, nil
} }
}
outputStr := string(output) outputStr := string(output)
var connUUID, connName string var connUUID, connName string
+42
View File
@@ -149,6 +149,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
} }
if strings.HasPrefix(req.Method, "clipboard.") { if strings.HasPrefix(req.Method, "clipboard.") {
switch req.Method {
case "clipboard.getConfig":
cfg := clipboard.LoadConfig()
models.Respond(conn, req.ID, cfg)
return
case "clipboard.setConfig":
handleClipboardSetConfig(conn, req)
return
}
if clipboardManager == nil { if clipboardManager == nil {
models.RespondError(conn, req.ID, "clipboard manager not initialized") models.RespondError(conn, req.ID, "clipboard manager not initialized")
return return
@@ -173,3 +182,36 @@ func RouteRequest(conn net.Conn, req models.Request) {
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
} }
} }
func handleClipboardSetConfig(conn net.Conn, req models.Request) {
cfg := clipboard.LoadConfig()
if v, ok := req.Params["maxHistory"].(float64); ok {
cfg.MaxHistory = int(v)
}
if v, ok := req.Params["maxEntrySize"].(float64); ok {
cfg.MaxEntrySize = int64(v)
}
if v, ok := req.Params["autoClearDays"].(float64); ok {
cfg.AutoClearDays = int(v)
}
if v, ok := req.Params["clearAtStartup"].(bool); ok {
cfg.ClearAtStartup = v
}
if v, ok := req.Params["disabled"].(bool); ok {
cfg.Disabled = v
}
if v, ok := req.Params["disableHistory"].(bool); ok {
cfg.DisableHistory = v
}
if v, ok := req.Params["disablePersist"].(bool); ok {
cfg.DisablePersist = v
}
if err := clipboard.SaveConfig(cfg); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "config updated"})
}
+33 -1
View File
@@ -33,7 +33,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 23 const APIVersion = 24
var CLIVersion = "dev" var CLIVersion = "dev"
@@ -702,6 +702,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("freedesktop.screensaver") && freedesktopManager != nil {
wg.Add(1)
screensaverChan := freedesktopManager.SubscribeScreensaver(clientID + "-screensaver")
go func() {
defer wg.Done()
defer freedesktopManager.UnsubscribeScreensaver(clientID + "-screensaver")
initialState := freedesktopManager.GetScreensaverState()
select {
case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-screensaverChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("gamma") && waylandManager != nil { if shouldSubscribe("gamma") && waylandManager != nil {
wg.Add(1) wg.Add(1)
waylandChan := waylandManager.Subscribe(clientID + "-gamma") waylandChan := waylandManager.Subscribe(clientID + "-gamma")
+58 -16
View File
@@ -103,18 +103,16 @@ func (m *Manager) waylandActor() {
} }
} }
func (m *Manager) allOutputsReady() bool { func (m *Manager) anyOutputReady() bool {
hasOutputs := false anyReady := false
allReady := true
m.outputs.Range(func(_ uint32, out *outputState) bool { m.outputs.Range(func(_ uint32, out *outputState) bool {
hasOutputs = true if out.rampSize > 0 && !out.failed {
if out.rampSize == 0 || out.failed { anyReady = true
allReady = false return false // stop iteration
return false
} }
return true return true
}) })
return hasOutputs && allReady return anyReady
} }
func (m *Manager) setupDBusMonitor() error { func (m *Manager) setupDBusMonitor() error {
@@ -278,7 +276,8 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
out.failed = false out.failed = false
out.retryCount = 0 out.retryCount = 0
} }
m.applyCurrentTemp() m.lastAppliedTemp = 0
m.applyCurrentTemp("gamma_size")
}) })
}) })
@@ -528,8 +527,9 @@ func (m *Manager) getNextDeadline(now time.Time) time.Time {
return m.tomorrow(now) return m.tomorrow(now)
case StateNormal: case StateNormal:
return m.getDeadlineNormal(now, sched) return m.getDeadlineNormal(now, sched)
} default:
return m.tomorrow(now) return m.tomorrow(now)
}
} }
func (m *Manager) getDeadlineNormal(now time.Time, sched sunSchedule) time.Time { func (m *Manager) getDeadlineNormal(now time.Time, sched sunSchedule) time.Time {
@@ -588,7 +588,7 @@ func (m *Manager) schedulerLoop() {
m.configMutex.RUnlock() m.configMutex.RUnlock()
if enabled { if enabled {
m.post(func() { m.applyCurrentTemp() }) m.post(func() { m.applyCurrentTemp("startup") })
} }
var timer *time.Timer var timer *time.Timer
@@ -630,24 +630,27 @@ func (m *Manager) schedulerLoop() {
enabled := m.config.Enabled enabled := m.config.Enabled
m.configMutex.RUnlock() m.configMutex.RUnlock()
if enabled { if enabled {
m.post(func() { m.applyCurrentTemp() }) m.post(func() { m.applyCurrentTemp("updateTrigger") })
} }
case <-timer.C: case <-timer.C:
m.configMutex.RLock() m.configMutex.RLock()
enabled := m.config.Enabled enabled := m.config.Enabled
m.configMutex.RUnlock() m.configMutex.RUnlock()
if enabled { if enabled {
m.post(func() { m.applyCurrentTemp() }) m.post(func() { m.applyCurrentTemp("timer") })
} }
} }
} }
} }
func (m *Manager) applyCurrentTemp() { func (m *Manager) applyCurrentTemp(_ string) {
if !m.controlsInitialized || !m.allOutputsReady() { if !m.controlsInitialized || !m.anyOutputReady() {
return return
} }
// Ensure schedule is up-to-date (handles display wake after overnight sleep)
m.recalcSchedule(time.Now())
m.configMutex.RLock() m.configMutex.RLock()
low, high := m.config.LowTemp, m.config.HighTemp low, high := m.config.LowTemp, m.config.HighTemp
m.configMutex.RUnlock() m.configMutex.RUnlock()
@@ -680,6 +683,10 @@ func (m *Manager) applyGamma(temp int) {
return return
} }
if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma {
return
}
var outs []*outputState var outs []*outputState
m.outputs.Range(func(_ uint32, out *outputState) bool { m.outputs.Range(func(_ uint32, out *outputState) bool {
outs = append(outs, out) outs = append(outs, out)
@@ -715,6 +722,7 @@ func (m *Manager) applyGamma(temp int) {
for _, j := range jobs { for _, j := range jobs {
if err := m.setGammaBytes(j.out, j.data); err != nil { if err := m.setGammaBytes(j.out, j.data); err != nil {
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
j.out.failed = true j.out.failed = true
j.out.rampSize = 0 j.out.rampSize = 0
outID := j.out.id outID := j.out.id
@@ -727,6 +735,9 @@ func (m *Manager) applyGamma(temp int) {
}) })
} }
} }
m.lastAppliedTemp = temp
m.lastAppliedGamma = gamma
} }
func (m *Manager) setGammaBytes(out *outputState, data []byte) error { func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
@@ -901,6 +912,10 @@ func (m *Manager) SetConfig(config Config) error {
func (m *Manager) SetTemperature(low, high int) error { func (m *Manager) SetTemperature(low, high int) error {
m.configMutex.Lock() m.configMutex.Lock()
if m.config.LowTemp == low && m.config.HighTemp == high {
m.configMutex.Unlock()
return nil
}
m.config.LowTemp = low m.config.LowTemp = low
m.config.HighTemp = high m.config.HighTemp = high
err := m.config.Validate() err := m.config.Validate()
@@ -914,6 +929,11 @@ func (m *Manager) SetTemperature(low, high int) error {
func (m *Manager) SetLocation(lat, lon float64) error { func (m *Manager) SetLocation(lat, lon float64) error {
m.configMutex.Lock() m.configMutex.Lock()
if m.config.Latitude != nil && m.config.Longitude != nil &&
*m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
m.configMutex.Unlock()
return nil
}
m.config.Latitude = &lat m.config.Latitude = &lat
m.config.Longitude = &lon m.config.Longitude = &lon
m.config.UseIPLocation = false m.config.UseIPLocation = false
@@ -928,6 +948,10 @@ func (m *Manager) SetLocation(lat, lon float64) error {
func (m *Manager) SetUseIPLocation(use bool) { func (m *Manager) SetUseIPLocation(use bool) {
m.configMutex.Lock() m.configMutex.Lock()
if m.config.UseIPLocation == use {
m.configMutex.Unlock()
return
}
m.config.UseIPLocation = use m.config.UseIPLocation = use
if use { if use {
m.config.Latitude = nil m.config.Latitude = nil
@@ -946,6 +970,12 @@ func (m *Manager) SetUseIPLocation(use bool) {
func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error { func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
m.configMutex.Lock() m.configMutex.Lock()
if m.config.ManualSunrise != nil && m.config.ManualSunset != nil &&
m.config.ManualSunrise.Hour() == sunrise.Hour() && m.config.ManualSunrise.Minute() == sunrise.Minute() &&
m.config.ManualSunset.Hour() == sunset.Hour() && m.config.ManualSunset.Minute() == sunset.Minute() {
m.configMutex.Unlock()
return nil
}
m.config.ManualSunrise = &sunrise m.config.ManualSunrise = &sunrise
m.config.ManualSunset = &sunset m.config.ManualSunset = &sunset
err := m.config.Validate() err := m.config.Validate()
@@ -959,6 +989,10 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
func (m *Manager) ClearManualTimes() { func (m *Manager) ClearManualTimes() {
m.configMutex.Lock() m.configMutex.Lock()
if m.config.ManualSunrise == nil && m.config.ManualSunset == nil {
m.configMutex.Unlock()
return
}
m.config.ManualSunrise = nil m.config.ManualSunrise = nil
m.config.ManualSunset = nil m.config.ManualSunset = nil
m.configMutex.Unlock() m.configMutex.Unlock()
@@ -967,6 +1001,10 @@ func (m *Manager) ClearManualTimes() {
func (m *Manager) SetGamma(gamma float64) error { func (m *Manager) SetGamma(gamma float64) error {
m.configMutex.Lock() m.configMutex.Lock()
if m.config.Gamma == gamma {
m.configMutex.Unlock()
return nil
}
m.config.Gamma = gamma m.config.Gamma = gamma
err := m.config.Validate() err := m.config.Validate()
m.configMutex.Unlock() m.configMutex.Unlock()
@@ -980,6 +1018,10 @@ func (m *Manager) SetGamma(gamma float64) error {
func (m *Manager) SetEnabled(enabled bool) { func (m *Manager) SetEnabled(enabled bool) {
m.configMutex.Lock() m.configMutex.Lock()
wasEnabled := m.config.Enabled wasEnabled := m.config.Enabled
if wasEnabled == enabled {
m.configMutex.Unlock()
return
}
m.config.Enabled = enabled m.config.Enabled = enabled
highTemp := m.config.HighTemp highTemp := m.config.HighTemp
m.configMutex.Unlock() m.configMutex.Unlock()
@@ -989,7 +1031,7 @@ func (m *Manager) SetEnabled(enabled bool) {
m.post(func() { m.post(func() {
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil { if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
log.Errorf("Failed to create gamma controls: %v", err) log.Errorf("gamma: failed to create controls: %v", err)
return return
} }
m.controlsInitialized = true m.controlsInitialized = true
+3
View File
@@ -96,6 +96,9 @@ type Manager struct {
dbusConn *dbus.Conn dbusConn *dbus.Conn
dbusSignal chan *dbus.Signal dbusSignal chan *dbus.Signal
lastAppliedTemp int
lastAppliedGamma float64
} }
type outputState struct { type outputState struct {
+59 -11
View File
@@ -1,11 +1,11 @@
package wlcontext package wlcontext
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"sync" "sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -27,6 +27,8 @@ type SharedContext struct {
stopChan chan struct{} stopChan chan struct{}
fatalError chan error fatalError chan error
cmdQueue chan func() cmdQueue chan func()
wakeR int
wakeW int
wg sync.WaitGroup wg sync.WaitGroup
mu sync.Mutex mu sync.Mutex
started bool started bool
@@ -38,11 +40,31 @@ func New() (*SharedContext, error) {
return nil, fmt.Errorf("%w: %v", errdefs.ErrNoWaylandDisplay, err) return nil, fmt.Errorf("%w: %v", errdefs.ErrNoWaylandDisplay, err)
} }
fds := make([]int, 2)
if err := unix.Pipe(fds); err != nil {
display.Context().Close()
return nil, fmt.Errorf("failed to create wake pipe: %w", err)
}
if err := unix.SetNonblock(fds[0], true); err != nil {
unix.Close(fds[0])
unix.Close(fds[1])
display.Context().Close()
return nil, fmt.Errorf("failed to set wake pipe nonblock: %w", err)
}
if err := unix.SetNonblock(fds[1], true); err != nil {
unix.Close(fds[0])
unix.Close(fds[1])
display.Context().Close()
return nil, fmt.Errorf("failed to set wake pipe nonblock: %w", err)
}
sc := &SharedContext{ sc := &SharedContext{
display: display, display: display,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
fatalError: make(chan error, 1), fatalError: make(chan error, 1),
cmdQueue: make(chan func(), 256), cmdQueue: make(chan func(), 256),
wakeR: fds[0],
wakeW: fds[1],
started: false, started: false,
} }
@@ -69,6 +91,9 @@ func (sc *SharedContext) Display() *wlclient.Display {
func (sc *SharedContext) Post(fn func()) { func (sc *SharedContext) Post(fn func()) {
select { select {
case sc.cmdQueue <- fn: case sc.cmdQueue <- fn:
if _, err := unix.Write(sc.wakeW, []byte{1}); err != nil && err != unix.EAGAIN {
log.Errorf("wake pipe write error: %v", err)
}
default: default:
} }
} }
@@ -89,7 +114,14 @@ func (sc *SharedContext) eventDispatcher() {
} }
} }
}() }()
ctx := sc.display.Context() ctx := sc.display.Context()
wlFd := ctx.Fd()
pollFds := []unix.PollFd{
{Fd: int32(wlFd), Events: unix.POLLIN},
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
}
for { for {
select { select {
@@ -100,22 +132,35 @@ func (sc *SharedContext) eventDispatcher() {
sc.drainCmdQueue() sc.drainCmdQueue()
if err := ctx.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil { n, err := unix.Poll(pollFds, 50)
log.Errorf("Failed to set read deadline: %v", err) if err != nil {
if err == unix.EINTR {
continue
} }
err := ctx.Dispatch() log.Errorf("Poll error: %v", err)
if err := ctx.SetReadDeadline(time.Time{}); err != nil { return
log.Errorf("Failed to clear read deadline: %v", err)
} }
switch { if n == 0 {
case err == nil: continue
case errors.Is(err, os.ErrDeadlineExceeded): }
default:
if pollFds[1].Revents&unix.POLLIN != 0 {
var buf [64]byte
if _, err := unix.Read(sc.wakeR, buf[:]); err != nil && err != unix.EAGAIN {
log.Errorf("wake pipe read error: %v", err)
}
}
if pollFds[0].Revents&unix.POLLIN != 0 {
if err := ctx.Dispatch(); err != nil {
if !os.IsTimeout(err) {
log.Errorf("Wayland connection error: %v", err) log.Errorf("Wayland connection error: %v", err)
return return
} }
} }
}
}
} }
func (sc *SharedContext) drainCmdQueue() { func (sc *SharedContext) drainCmdQueue() {
@@ -133,6 +178,9 @@ func (sc *SharedContext) Close() {
close(sc.stopChan) close(sc.stopChan)
sc.wg.Wait() sc.wg.Wait()
unix.Close(sc.wakeR)
unix.Close(sc.wakeW)
if sc.display != nil { if sc.display != nil {
sc.display.Context().Close() sc.display.Context().Close()
} }
+31 -15
View File
@@ -125,21 +125,47 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
return return
} }
statusChan := make(chan error, 1) responded := false
config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) { config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) {
if responded {
return
}
responded = true
log.Info("WlrOutput: configuration succeeded") log.Info("WlrOutput: configuration succeeded")
statusChan <- nil config.Destroy()
resultChan <- nil
}) })
config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) { config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) {
if responded {
return
}
responded = true
log.Warn("WlrOutput: configuration failed") log.Warn("WlrOutput: configuration failed")
statusChan <- fmt.Errorf("compositor rejected configuration") config.Destroy()
resultChan <- fmt.Errorf("compositor rejected configuration")
}) })
config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) { config.SetCancelledHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1CancelledEvent) {
if responded {
return
}
responded = true
log.Warn("WlrOutput: configuration cancelled") log.Warn("WlrOutput: configuration cancelled")
statusChan <- fmt.Errorf("configuration cancelled (outdated serial)") config.Destroy()
resultChan <- fmt.Errorf("configuration cancelled (outdated serial)")
})
time.AfterFunc(time.Second, func() {
m.post(func() {
if responded {
return
}
responded = true
config.Destroy()
resultChan <- fmt.Errorf("timeout waiting for configuration response")
})
}) })
headsByName := make(map[string]*headState) headsByName := make(map[string]*headState)
@@ -241,6 +267,7 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
} }
if applyErr != nil { if applyErr != nil {
responded = true
config.Destroy() config.Destroy()
action := "apply" action := "apply"
if test { if test {
@@ -249,17 +276,6 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr) resultChan <- fmt.Errorf("failed to %s configuration: %w", action, applyErr)
return return
} }
go func() {
select {
case err := <-statusChan:
config.Destroy()
resultChan <- err
case <-time.After(5 * time.Second):
config.Destroy()
resultChan <- fmt.Errorf("timeout waiting for configuration response")
}
}()
}) })
return <-resultChan return <-resultChan
+7 -2
View File
@@ -145,6 +145,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name) log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
head.name = e.Name head.name = e.Name
head.ready = true
m.post(func() { m.post(func() {
m.updateState() m.updateState()
}) })
@@ -251,11 +252,11 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
m.heads.Delete(headID) m.heads.Delete(headID)
m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Release() handle.Release()
m.wlMutex.Unlock() m.wlMutex.Unlock()
m.post(func() {
m.updateState() m.updateState()
}) })
}) })
@@ -310,11 +311,11 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
m.modes.Delete(modeID) m.modes.Delete(modeID)
m.post(func() {
m.wlMutex.Lock() m.wlMutex.Lock()
handle.Release() handle.Release()
m.wlMutex.Unlock() m.wlMutex.Unlock()
m.post(func() {
m.updateState() m.updateState()
}) })
}) })
@@ -328,6 +329,10 @@ func (m *Manager) updateState() {
return true return true
} }
if !head.ready {
return true
}
modes := make([]OutputMode, 0) modes := make([]OutputMode, 0)
var currentMode *OutputMode var currentMode *OutputMode
+1
View File
@@ -90,6 +90,7 @@ type headState struct {
modeIDs []uint32 modeIDs []uint32
adaptiveSync uint32 adaptiveSync uint32
finished bool finished bool
ready bool
} }
type modeState struct { type modeState struct {
@@ -58,6 +58,18 @@ func (ctx *Context) SetReadDeadline(t time.Time) error {
return ctx.conn.SetReadDeadline(t) return ctx.conn.SetReadDeadline(t)
} }
func (ctx *Context) Fd() int {
rawConn, err := ctx.conn.SyscallConn()
if err != nil {
return -1
}
var fd int
rawConn.Control(func(f uintptr) {
fd = int(f)
})
return fd
}
// Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the // Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the
// respective wayland protocol. // respective wayland protocol.
// Dispatch must be called on the same goroutine as other interactions with the Context. // Dispatch must be called on the same goroutine as other interactions with the Context.
+36
View File
@@ -1,3 +1,39 @@
dms-git (1.0.2+git2528.d336866f) nightly; urgency=medium
* Git snapshot (commit 2528: d336866f)
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 14 Dec 2025 03:56:25 +0000
dms-git (1.0.2+git2521.3b511e2f) nightly; urgency=medium
* Git snapshot (commit 2521: 3b511e2f)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 21:10:22 +0000
dms-git (1.0.2+git2518.a783d650) nightly; urgency=medium
* Git snapshot (commit 2518: a783d650)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 15:11:40 +0000
dms-git (1.0.2+git2510.0f89886c) nightly; urgency=medium
* Git snapshot (commit 2510: 0f89886c)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:46:43 +0000
dms-git (1.0.2+git2507.b2ac9c6c) nightly; urgency=medium
* Git snapshot (commit 2507: b2ac9c6c)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:18:05 +0000
dms-git (1.0.2+git2505.82f881af) nightly; urgency=medium
* Git snapshot (commit 2505: 82f881af)
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 05:55:03 +0000
dms-git (1.0.0+git2419.993f14a3) nightly; urgency=medium dms-git (1.0.0+git2419.993f14a3) nightly; urgency=medium
* Major stable release v1.0.0 * Major stable release v1.0.0
+3 -4
View File
@@ -1,9 +1,8 @@
dms (1.0.2) stable; urgency=medium dms (1.0.2ppa6) unstable; urgency=medium
* Update to v1.0.2 stable release * Rebuild to fix repository metadata issues
* Bug fixes and improvements
-- Avenge Media <AvengeMedia.US@gmail.com> Thu, 12 Dec 2025 14:30:00 -0500 -- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:47:39 +0000
dms (1.0.0) stable; urgency=medium dms (1.0.0) stable; urgency=medium
+13 -1
View File
@@ -1,7 +1,7 @@
%global debug_package %{nil} %global debug_package %{nil}
Name: dms-git Name: dms-git
Version: 0.6.2+git2147.03073f68 Version: 1.0.2+git2528.d336866f
Release: 1%{?dist} Release: 1%{?dist}
Epoch: 2 Epoch: 2
Summary: DankMaterialShell - Material 3 inspired shell (git nightly) Summary: DankMaterialShell - Material 3 inspired shell (git nightly)
@@ -135,6 +135,18 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
%changelog %changelog
* Sun Dec 14 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2528.d336866f-1
- Git snapshot (commit 2528: d336866f)
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2521.3b511e2f-1
- Git snapshot (commit 2521: 3b511e2f)
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2518.a783d650-1
- Git snapshot (commit 2518: a783d650)
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2510.0f89886c-1
- Git snapshot (commit 2510: 0f89886c)
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2507.b2ac9c6c-1
- Git snapshot (commit 2507: b2ac9c6c)
* Sat Dec 13 2025 Avenge Media <AvengeMedia.US@gmail.com> - 1.0.2+git2505.82f881af-1
- Git snapshot (commit 2505: 82f881af)
* Tue Nov 25 2025 Avenge Media <AvengeMedia.US@gmail.com> - 0.6.2+git2147.03073f68-1 * Tue Nov 25 2025 Avenge Media <AvengeMedia.US@gmail.com> - 0.6.2+git2147.03073f68-1
- Git snapshot (commit 2147: 03073f68) - Git snapshot (commit 2147: 03073f68)
* Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2+git-5 * Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2+git-5
+1 -1
View File
@@ -4,7 +4,7 @@
Name: dms Name: dms
Version: 1.0.2 Version: 1.0.2
Release: 1%{?dist} Release: 7%{?dist}
Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
License: MIT License: MIT
+218 -298
View File
@@ -1,12 +1,14 @@
#!/bin/bash #!/bin/bash
# Unified OBS upload script for dms packages # Unified OBS upload script for dms packages
# Handles Debian and OpenSUSE builds for both x86_64 and aarch64 # Handles Debian and OpenSUSE builds for both x86_64 and aarch64
# Usage: ./distro/scripts/obs-upload.sh [distro] <package-name> [commit-message] # Usage: ./distro/scripts/obs-upload.sh [distro] <package-name> [commit-message|rebuild-number]
# #
# Examples: # Examples:
# ./distro/scripts/obs-upload.sh dms "Update to v0.6.2" # ./distro/scripts/obs-upload.sh dms "Update to v1.0.2"
# ./distro/scripts/obs-upload.sh debian dms # ./distro/scripts/obs-upload.sh debian dms
# ./distro/scripts/obs-upload.sh opensuse dms-git # ./distro/scripts/obs-upload.sh opensuse dms-git
# ./distro/scripts/obs-upload.sh debian dms-git 2 # Rebuild with ppa2 suffix
# ./distro/scripts/obs-upload.sh dms-git --rebuild=2 # Rebuild with ppa2 suffix (flag syntax)
set -e set -e
@@ -14,6 +16,8 @@ UPLOAD_DEBIAN=true
UPLOAD_OPENSUSE=true UPLOAD_OPENSUSE=true
PACKAGE="" PACKAGE=""
MESSAGE="" MESSAGE=""
REBUILD_RELEASE="${REBUILD_RELEASE:-}"
POSITIONAL_ARGS=()
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
@@ -25,16 +29,43 @@ for arg in "$@"; do
UPLOAD_DEBIAN=false UPLOAD_DEBIAN=false
UPLOAD_OPENSUSE=true UPLOAD_OPENSUSE=true
;; ;;
--rebuild=*)
REBUILD_RELEASE="${arg#*=}"
;;
-r|--rebuild)
REBUILD_NEXT=true
;;
*) *)
if [[ -z "$PACKAGE" ]]; then if [[ -n "${REBUILD_NEXT:-}" ]]; then
PACKAGE="$arg" REBUILD_RELEASE="$arg"
elif [[ -z "$MESSAGE" ]]; then REBUILD_NEXT=false
MESSAGE="$arg" else
POSITIONAL_ARGS+=("$arg")
fi fi
;; ;;
esac esac
done done
# Check if last positional argument is a number (rebuild release)
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
LAST_ARG="${POSITIONAL_ARGS[$LAST_INDEX]}"
if [[ "$LAST_ARG" =~ ^[0-9]+$ ]] && [[ -z "$REBUILD_RELEASE" ]]; then
# Last argument is a number and no --rebuild flag was used
# Use it as rebuild release and remove from positional args
REBUILD_RELEASE="$LAST_ARG"
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
fi
fi
# Assign remaining positional args to PACKAGE and MESSAGE
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
PACKAGE="${POSITIONAL_ARGS[0]}"
if [[ ${#POSITIONAL_ARGS[@]} -gt 1 ]]; then
MESSAGE="${POSITIONAL_ARGS[1]}"
fi
fi
OBS_BASE_PROJECT="home:AvengeMedia" OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts" OBS_BASE="$HOME/.cache/osc-checkouts"
AVAILABLE_PACKAGES=(dms dms-git) AVAILABLE_PACKAGES=(dms dms-git)
@@ -70,6 +101,51 @@ if [[ ! -d "distro/debian" ]]; then
echo "Error: Run this script from the repository root" echo "Error: Run this script from the repository root"
exit 1 exit 1
fi fi
# Parameters:
# $1 = PROJECT
# $2 = PACKAGE
# $3 = VERSION
# $4 = CHECK_MODE - Exact version match, "commit" = check commit hash (default)
check_obs_version_exists() {
local PROJECT="$1"
local PACKAGE="$2"
local VERSION="$3"
local CHECK_MODE="${4:-commit}"
local OBS_SPEC=""
# Use osc api command (works in both local and CI environments)
if command -v osc &> /dev/null; then
OBS_SPEC=$(osc api "/source/$PROJECT/$PACKAGE/${PACKAGE}.spec" 2>/dev/null || echo "")
else
echo "⚠️ osc command not found, skipping version check"
return 1
fi
# Check if we got valid spec content
if [[ -n "$OBS_SPEC" && "$OBS_SPEC" != *"error"* && "$OBS_SPEC" == *"Version:"* ]]; then
OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs)
# Commit hash check for -git packages
if [[ "$CHECK_MODE" == "commit" ]] && [[ "$PACKAGE" == *"-git" ]]; then
OBS_COMMIT=$(echo "$OBS_VERSION" | grep -oP '\.([a-f0-9]{8})(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
NEW_COMMIT=$(echo "$VERSION" | grep -oP '\.([a-f0-9]{8})(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
if [[ -n "$OBS_COMMIT" && -n "$NEW_COMMIT" && "$OBS_COMMIT" == "$NEW_COMMIT" ]]; then
echo "⚠️ Commit $NEW_COMMIT already exists in OBS (current version: $OBS_VERSION)"
return 0
fi
fi
# Exact version match check
if [[ "$OBS_VERSION" == "$VERSION" ]]; then
echo "⚠️ Version $VERSION already exists in OBS"
return 0
fi
else
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
return 1
fi
return 1
}
# Handle "all" option # Handle "all" option
if [[ "$PACKAGE" == "all" ]]; then if [[ "$PACKAGE" == "all" ]]; then
@@ -145,9 +221,9 @@ IS_MANUAL=false
if [[ -n "${REBUILD_RELEASE:-}" ]]; then if [[ -n "${REBUILD_RELEASE:-}" ]]; then
IS_MANUAL=true IS_MANUAL=true
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)" echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then elif [[ -n "${FORCE_UPLOAD:-}" ]] && [[ "${FORCE_UPLOAD}" == "true" ]]; then
IS_MANUAL=true IS_MANUAL=true
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)" echo "==> Force upload detected (FORCE_UPLOAD=true)"
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
IS_MANUAL=true IS_MANUAL=true
echo "==> Local/manual run detected (not in CI)" echo "==> Local/manual run detected (not in CI)"
@@ -183,7 +259,15 @@ fi
CHANGELOG_VERSION="" CHANGELOG_VERSION=""
if [[ -d "distro/debian/$PACKAGE/debian" ]]; then if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
# Format: 0.6.2+git{COMMIT_COUNT}.{COMMIT_HASH} (e.g., 0.6.2+git2256.9162e314) # For -git packages, generate version dynamically from git state (like workflows do)
if [[ "$PACKAGE" == *"-git" ]]; then
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
CHANGELOG_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo " - Generated git snapshot version: $CHANGELOG_VERSION"
else
# For stable packages: Format: 0.6.2+git{COMMIT_COUNT}.{COMMIT_HASH}
CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" "distro/debian/$PACKAGE/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "") CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" "distro/debian/$PACKAGE/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "")
if [[ -n "$CHANGELOG_VERSION" ]] && [[ "$CHANGELOG_VERSION" == *"-"* ]]; then if [[ -n "$CHANGELOG_VERSION" ]] && [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
SOURCE_FORMAT_CHECK=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)") SOURCE_FORMAT_CHECK=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
@@ -191,6 +275,47 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
CHANGELOG_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/-[0-9]*$//') CHANGELOG_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/-[0-9]*$//')
fi fi
fi fi
fi
# Apply rebuild suffix if specified (must happen before API check)
if [[ -n "$REBUILD_RELEASE" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
CHANGELOG_VERSION="${CHANGELOG_VERSION}ppa${REBUILD_RELEASE}"
echo " - Applied rebuild suffix: $CHANGELOG_VERSION"
fi
# Check if this version already exists in OBS
if [[ -n "$CHANGELOG_VERSION" ]]; then
if [[ -z "$REBUILD_RELEASE" ]]; then
if check_obs_version_exists "$OBS_PROJECT" "$PACKAGE" "$CHANGELOG_VERSION"; then
if [[ "$PACKAGE" == *"-git" ]]; then
echo "==> Error: This commit is already uploaded to OBS"
echo " The same git commit ($(echo "$CHANGELOG_VERSION" | grep -oP '[a-f0-9]{8}' | tail -1)) already exists on OBS."
echo " To rebuild the same commit, specify a rebuild number:"
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
echo " ./distro/scripts/obs-upload.sh $PACKAGE 3"
echo " Or push a new commit first, then run:"
echo " ./distro/scripts/obs-upload.sh $PACKAGE"
else
echo "==> Error: Version $CHANGELOG_VERSION already exists in OBS"
echo " To rebuild with a different release number, try:"
echo " ./distro/scripts/obs-upload.sh $PACKAGE --rebuild=2"
echo " or positional syntax:"
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
fi
exit 1
fi
else
# Rebuild number specified - check if this exact version already exists (exact mode)
if check_obs_version_exists "$OBS_PROJECT" "$PACKAGE" "$CHANGELOG_VERSION" "exact"; then
echo "==> Error: Version $CHANGELOG_VERSION already exists in OBS"
echo " This exact version (including ppa${REBUILD_RELEASE}) is already uploaded."
echo " To rebuild with a different release number, try incrementing:"
NEXT_NUM=$((REBUILD_RELEASE + 1))
echo " ./distro/scripts/obs-upload.sh $PACKAGE $NEXT_NUM"
exit 1
fi
fi
fi
fi fi
if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
@@ -204,25 +329,28 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
OLD_RELEASE=$(grep "^Release:" "$WORK_DIR/.osc/$PACKAGE.spec" | sed 's/^Release:[[:space:]]*//' | sed 's/%{?dist}//' | head -1) OLD_RELEASE=$(grep "^Release:" "$WORK_DIR/.osc/$PACKAGE.spec" | sed 's/^Release:[[:space:]]*//' | sed 's/%{?dist}//' | head -1)
if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then
if [[ "$OLD_RELEASE" =~ ^([0-9]+) ]]; then if [[ "$IS_MANUAL" == true ]] && [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
BASE_RELEASE="${BASH_REMATCH[1]}" # Only error for true local manual runs, not CI/workflow runs
if [[ "$IS_MANUAL" == true ]]; then if [[ -n "${REBUILD_RELEASE:-}" ]]; then
NEXT_RELEASE=$((BASE_RELEASE + 1)) echo " 🔄 Using manual rebuild release number: $REBUILD_RELEASE"
echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)" sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${REBUILD_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${NEXT_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec" cp "$WORK_DIR/$PACKAGE.spec" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec"
else else
echo " - Detected same version $NEW_VERSION (release $OLD_RELEASE). Not a manual run, skipping update." echo " - Error: Same version detected ($NEW_VERSION) but no rebuild number specified"
# For automated runs with no version change, we should stop here to avoid unnecessary rebuilds echo " To rebuild, explicitly specify a rebuild number:"
# However, we need to check if we are also updating Debian, or if this script is expected to continue. echo " ./distro/scripts/obs-upload.sh opensuse $PACKAGE 2"
# If this is OpenSUSE only run, we can exit. echo " or use flag syntax:"
if [[ "$UPLOAD_DEBIAN" == false ]]; then echo " ./distro/scripts/obs-upload.sh opensuse $PACKAGE --rebuild=2"
echo "✅ No changes needed for OpenSUSE (not manual). Exiting." exit 1
fi
else
echo " - Detected same version $NEW_VERSION (release $OLD_RELEASE). No changes needed, skipping update."
echo "✅ No changes needed for this package. Exiting gracefully."
exit 0 exit 0
fi fi
fi
fi
else else
echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)" echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)"
cp "$WORK_DIR/$PACKAGE.spec" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec"
fi fi
else else
echo " - First upload to OBS (no previous spec found)" echo " - First upload to OBS (no previous spec found)"
@@ -518,13 +646,47 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
echo " - OpenSUSE source tarballs created" echo " - OpenSUSE source tarballs created"
fi fi
# Copy and update OpenSUSE spec file with the correct version (for -git packages)
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/" cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
echo " Updating OpenSUSE spec to version $CHANGELOG_VERSION"
sed -i "s/^Version:.*/Version: $CHANGELOG_VERSION/" "$WORK_DIR/$PACKAGE.spec"
# Update changelog in spec file
DATE_STR=$(date "+%a %b %d %Y")
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' "$WORK_DIR/$PACKAGE.spec")
{
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${CHANGELOG_VERSION}-1"
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
} > "$WORK_DIR/$PACKAGE.spec"
fi
fi fi
if [[ "$UPLOAD_DEBIAN" == true ]]; then if [[ "$UPLOAD_DEBIAN" == true ]]; then
echo " Copying debian/ directory into source" echo " Copying debian/ directory into source"
cp -r "distro/debian/$PACKAGE/debian" "$SOURCE_DIR/" cp -r "distro/debian/$PACKAGE/debian" "$SOURCE_DIR/"
# Update changelog with the correct version (for -git packages, use dynamically generated version)
if [[ -n "$CHANGELOG_VERSION" ]] && [[ -f "$SOURCE_DIR/debian/changelog" ]]; then
echo " Updating changelog to version $CHANGELOG_VERSION"
TEMP_CHANGELOG=$(mktemp)
{
echo "$PACKAGE ($CHANGELOG_VERSION) unstable; urgency=medium"
echo ""
if [[ "$PACKAGE" == *"-git" ]]; then
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
else
echo " * Automated update"
fi
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
} >"$TEMP_CHANGELOG"
cp "$TEMP_CHANGELOG" "$SOURCE_DIR/debian/changelog"
rm -f "$TEMP_CHANGELOG"
fi
# For dms, rename directory to match what debian/rules expects # For dms, rename directory to match what debian/rules expects
# debian/rules uses UPSTREAM_VERSION which is the full version from changelog # debian/rules uses UPSTREAM_VERSION which is the full version from changelog
if [[ "$PACKAGE" == "dms" ]]; then if [[ "$PACKAGE" == "dms" ]]; then
@@ -636,293 +798,51 @@ fi
cd "$WORK_DIR" cd "$WORK_DIR"
echo "==> Updating working copy" # Server-side cleanup via API
if ! osc up; then echo "==> Cleaning old tarballs from OBS server (prevents downloading 100+ old versions)"
echo "Error: Failed to update working copy" OBS_FILES=$(osc api "/source/$OBS_PROJECT/$PACKAGE" 2>/dev/null || echo "")
exit 1 if [[ -n "$OBS_FILES" ]]; then
fi DELETED_COUNT=0
KEEP_PATTERN=""
# Only auto-increment on manual runs (REBUILD_RELEASE set or not in CI), not automated workflows if [[ -n "$CHANGELOG_VERSION" ]]; then
OLD_DSC_FILE="" BASE_KEEP_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
if [[ -f "$WORK_DIR/.osc/sources/$PACKAGE.dsc" ]]; then KEEP_PATTERN="${PACKAGE}_${BASE_KEEP_VERSION}"
OLD_DSC_FILE="$WORK_DIR/.osc/sources/$PACKAGE.dsc" echo " Keeping tarballs matching: ${KEEP_PATTERN}*"
elif [[ -f "$WORK_DIR/.osc/$PACKAGE.dsc" ]]; then
OLD_DSC_FILE="$WORK_DIR/.osc/$PACKAGE.dsc"
fi
if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[ -n "$OLD_DSC_FILE" ]]; then
OLD_DSC_VERSION=$(grep "^Version:" "$OLD_DSC_FILE" 2>/dev/null | awk '{print $2}' | head -1)
IS_MANUAL=false
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
IS_MANUAL=true
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then
IS_MANUAL=true
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)"
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
IS_MANUAL=true
echo "==> Local/manual run detected (not in CI)"
fi fi
CHANGELOG_BASE=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//') for old_file in $(echo "$OBS_FILES" | grep -oP '(?<=name=")[^"]*\.(tar\.gz|tar\.xz|tar\.bz2)(?=")' || true); do
OLD_DSC_BASE=$(echo "$OLD_DSC_VERSION" | sed 's/ppa[0-9]*$//') if [[ -n "$KEEP_PATTERN" ]] && [[ "$old_file" == ${KEEP_PATTERN}* ]]; then
echo " - Keeping current version: $old_file"
if [[ -n "$OLD_DSC_VERSION" ]] && [[ "$OLD_DSC_BASE" == "$CHANGELOG_BASE" ]]; then continue
if [[ "$IS_MANUAL" == true ]]; then
echo "==> Detected rebuild of same base version $CHANGELOG_BASE, incrementing version"
# If REBUILD_RELEASE is set, use that number directly
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
if [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git([0-9]+)(\.[a-f0-9]+)?$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
GIT_NUM="${BASH_REMATCH[2]}"
GIT_HASH="${BASH_REMATCH[3]}"
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${REBUILD_RELEASE}"
echo " Using REBUILD_RELEASE=$REBUILD_RELEASE: $CHANGELOG_VERSION -> $NEW_VERSION"
else
BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
NEW_VERSION="${BASE_VERSION}ppa${REBUILD_RELEASE}"
echo " Using REBUILD_RELEASE=$REBUILD_RELEASE: $CHANGELOG_VERSION -> $NEW_VERSION"
fi
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
NEW_VERSION="${BASE_VERSION}+gitppa1"
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)ppa([0-9]+)$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
PPA_NUM="${BASH_REMATCH[2]}"
NEW_PPA_NUM=$((PPA_NUM + 1))
NEW_VERSION="${BASE_VERSION}ppa${NEW_PPA_NUM}"
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git([0-9]+)(\.[a-f0-9]+)?(ppa([0-9]+))?$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
GIT_NUM="${BASH_REMATCH[2]}"
GIT_HASH="${BASH_REMATCH[3]}"
PPA_NUM="${BASH_REMATCH[5]}"
# Check if old DSC has ppa suffix even if changelog doesn't
if [[ -z "$PPA_NUM" ]] && [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
OLD_PPA_NUM="${BASH_REMATCH[1]}"
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
elif [[ -n "$PPA_NUM" ]]; then
NEW_PPA_NUM=$((PPA_NUM + 1))
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
else
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa1"
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
fi
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)(-([0-9]+))?$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
# Check if old DSC has ppa suffix even if changelog doesn't
if [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
OLD_PPA_NUM="${BASH_REMATCH[1]}"
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
NEW_VERSION="${BASE_VERSION}ppa${NEW_PPA_NUM}"
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
else
NEW_VERSION="${BASE_VERSION}ppa1"
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
fi
else
# Check if old DSC has ppa suffix for unknown formats
if [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
OLD_PPA_NUM="${BASH_REMATCH[1]}"
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
NEW_VERSION="${CHANGELOG_VERSION}ppa${NEW_PPA_NUM}"
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
else
NEW_VERSION="${CHANGELOG_VERSION}ppa1"
echo " Warning: Could not parse version format, appending ppa1: $CHANGELOG_VERSION -> $NEW_VERSION"
fi
fi fi
if [[ -z "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR/debian" ]]; then if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
echo " Error: Source directory with debian/ not found for version increment" echo " - Keeping source tarball: $old_file"
exit 1 continue
fi fi
SOURCE_CHANGELOG="$SOURCE_DIR/debian/changelog" echo " - Deleting from server: $old_file"
if [[ ! -f "$SOURCE_CHANGELOG" ]]; then if osc api -X DELETE "/source/$OBS_PROJECT/$PACKAGE/$old_file" 2>/dev/null; then
echo " Error: Changelog not found in source directory: $SOURCE_CHANGELOG" ((DELETED_COUNT++)) || true
exit 1
fi
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
TEMP_CHANGELOG=$(mktemp)
{
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
echo ""
echo " * Rebuild to fix repository metadata issues"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
echo ""
if [[ -f "$REPO_CHANGELOG" ]]; then
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
if [[ -n "$OLD_ENTRY_START" ]]; then
tail -n +"$OLD_ENTRY_START" "$REPO_CHANGELOG"
fi
fi
} >"$TEMP_CHANGELOG"
cp "$TEMP_CHANGELOG" "$SOURCE_CHANGELOG"
rm -f "$TEMP_CHANGELOG"
CHANGELOG_VERSION="$NEW_VERSION"
VERSION="$NEW_VERSION"
COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
for old_tarball in "${PACKAGE}"_*.tar.gz; do
if [[ -f "$old_tarball" ]] && [[ "$old_tarball" != "${PACKAGE}_${NEW_VERSION}.tar.gz" ]]; then
echo " Removing old tarball from OBS: $old_tarball"
osc rm -f "$old_tarball" 2>/dev/null || rm -f "$old_tarball"
fi fi
done done
if [[ $DELETED_COUNT -gt 0 ]]; then
if [[ "$PACKAGE" == "dms" ]] && [[ -f "$WORK_DIR/dms-source.tar.gz" ]]; then echo " ✓ Deleted $DELETED_COUNT old tarball(s) from server"
echo " Recreating dms-source.tar.gz with new directory name for incremented version"
EXPECTED_SOURCE_DIR="DankMaterialShell-${NEW_VERSION}"
TEMP_SOURCE_DIR=$(mktemp -d)
cd "$TEMP_SOURCE_DIR"
tar -xzf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xJf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xjf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null
EXTRACTED=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
if [[ -n "$EXTRACTED" ]] && [[ "$EXTRACTED" != "./$EXPECTED_SOURCE_DIR" ]]; then
echo " Renaming $EXTRACTED to $EXPECTED_SOURCE_DIR"
mv "$EXTRACTED" "$EXPECTED_SOURCE_DIR"
rm -f "$WORK_DIR/dms-source.tar.gz"
if ! tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/dms-source.tar.gz" "$EXPECTED_SOURCE_DIR"; then
echo " Error: Failed to create dms-source.tar.gz"
ls -lah "$EXPECTED_SOURCE_DIR" | head -20
exit 1
fi
if [[ ! -f "$WORK_DIR/dms-source.tar.gz" ]]; then
echo " Error: dms-source.tar.gz was not created"
exit 1
fi
ROOT_DIR=$(tar -tf "$WORK_DIR/dms-source.tar.gz" | head -1 | cut -d/ -f1)
if [[ "$ROOT_DIR" != "$EXPECTED_SOURCE_DIR" ]]; then
echo " Error: Recreated tarball has wrong root directory: $ROOT_DIR (expected $EXPECTED_SOURCE_DIR)"
exit 1
fi
fi
cd "$REPO_ROOT"
rm -rf "$TEMP_SOURCE_DIR"
fi
echo " Recreating tarball with new version: $COMBINED_TARBALL"
if [[ -n "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR/debian" ]]; then
if [[ "$PACKAGE" == "dms" ]]; then
cd "$(dirname "$SOURCE_DIR")"
CURRENT_DIR=$(basename "$SOURCE_DIR")
EXPECTED_DIR="DankMaterialShell-${NEW_VERSION}"
if [[ "$CURRENT_DIR" != "$EXPECTED_DIR" ]]; then
echo " Renaming directory from $CURRENT_DIR to $EXPECTED_DIR to match debian/rules"
if [[ -d "$CURRENT_DIR" ]]; then
mv "$CURRENT_DIR" "$EXPECTED_DIR"
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
else else
echo " Warning: Source directory $CURRENT_DIR not found, extracting from existing tarball" echo " ✓ No old tarballs found on server (current version preserved)"
OLD_TARBALL=$(ls "${PACKAGE}"_*.tar.gz 2>/dev/null | head -1)
if [[ -f "$OLD_TARBALL" ]]; then
EXTRACT_DIR=$(mktemp -d)
cd "$EXTRACT_DIR"
tar -xzf "$WORK_DIR/$OLD_TARBALL"
EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
if [[ -n "$EXTRACTED_DIR" ]] && [[ "$EXTRACTED_DIR" != "./$EXPECTED_DIR" ]]; then
mv "$EXTRACTED_DIR" "$EXPECTED_DIR"
if [[ -f "$EXPECTED_DIR/debian/changelog" ]]; then
ACTUAL_VER=$(grep -m1 "^$PACKAGE" "$EXPECTED_DIR/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/')
if [[ "$ACTUAL_VER" != "$NEW_VERSION" ]]; then
echo " Updating changelog version in extracted directory"
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
TEMP_CHANGELOG=$(mktemp)
{
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
echo ""
echo " * Rebuild to fix repository metadata issues"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
echo ""
if [[ -f "$REPO_CHANGELOG" ]]; then
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
if [[ -n "$OLD_ENTRY_START" ]]; then
tail -n +"$OLD_ENTRY_START" "$REPO_CHANGELOG"
fi fi
fi else
} >"$TEMP_CHANGELOG" echo " ⚠️ Could not fetch file list from server, skipping cleanup"
cp "$TEMP_CHANGELOG" "$EXPECTED_DIR/debian/changelog" fi
rm -f "$TEMP_CHANGELOG"
fi # Fallback update with --server-side-source-service-files flag only syncs metadata (spec, dsc, _service)
fi echo "==> Updating working copy"
SOURCE_DIR="$(pwd)/$EXPECTED_DIR" if ! osc up --server-side-source-service-files 2>/dev/null; then
cd "$REPO_ROOT" echo " Note: Using regular update (--server-side-source-service-files not supported)"
else if ! osc up; then
echo " Error: Could not extract or find source directory" echo "Error: Failed to update working copy"
rm -rf "$EXTRACT_DIR"
exit 1 exit 1
fi fi
fi
fi
fi
fi
rm -f "$WORK_DIR/$COMBINED_TARBALL"
echo " Creating combined tarball: $COMBINED_TARBALL"
cd "$(dirname "$SOURCE_DIR")"
TARBALL_BASE=$(basename "$SOURCE_DIR")
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
cd "$REPO_ROOT"
fi
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
# Extract Build-Depends from debian/control using awk for proper multi-line parsing
if [[ -f "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" ]]; then
BUILD_DEPS=$(awk '
/^Build-Depends:/ {
in_build_deps=1;
sub(/^Build-Depends:[[:space:]]*/, "");
printf "%s", $0;
next;
}
in_build_deps && /^[[:space:]]/ {
sub(/^[[:space:]]+/, " ");
printf "%s", $0;
next;
}
in_build_deps { exit; }
' "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]*//; s/[[:space:]]*$//')
# If extraction failed or is empty, use default fallback
if [[ -z "$BUILD_DEPS" ]]; then
BUILD_DEPS="debhelper-compat (= 13)"
fi
else
BUILD_DEPS="debhelper-compat (= 13)"
fi
cat >"$WORK_DIR/$PACKAGE.dsc" <<EOF
Format: 3.0 (native)
Source: $PACKAGE
Binary: $PACKAGE
Architecture: any
Version: $VERSION
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: $BUILD_DEPS
Files:
$TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL
EOF
echo " - Updated changelog and recreated tarball with version $NEW_VERSION"
else
echo "==> Detected same version. Not a manual run, skipping Debian version increment."
echo "✅ No changes needed for Debian. Exiting."
exit 0
fi
fi
fi fi
# Ensure we're in WORK_DIR and it exists # Ensure we're in WORK_DIR and it exists
+250 -299
View File
@@ -36,7 +36,6 @@ fi
PACKAGE_DIR="$1" PACKAGE_DIR="$1"
UBUNTU_SERIES="${2:-noble}" UBUNTU_SERIES="${2:-noble}"
# Validate package directory
if [ ! -d "$PACKAGE_DIR" ]; then if [ ! -d "$PACKAGE_DIR" ]; then
error "Package directory not found: $PACKAGE_DIR" error "Package directory not found: $PACKAGE_DIR"
exit 1 exit 1
@@ -47,21 +46,43 @@ if [ ! -d "$PACKAGE_DIR/debian" ]; then
exit 1 exit 1
fi fi
# Get absolute path
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd) PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
PACKAGE_NAME=$(basename "$PACKAGE_DIR") PACKAGE_NAME=$(basename "$PACKAGE_DIR")
PACKAGE_PARENT=$(dirname "$PACKAGE_DIR") PACKAGE_PARENT=$(dirname "$PACKAGE_DIR")
# Create temporary working directory (like OBS) # Choose temp directory: use /tmp in CI, ~/tmp locally (keeps artifacts out of repo)
TEMP_WORK_DIR=$(mktemp -d -t ppa_build_work_XXXXXX) if [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${CI:-}" ]]; then
trap 'rm -rf "$TEMP_WORK_DIR"' EXIT TEMP_BASE="/tmp"
else
TEMP_BASE="$HOME/tmp"
mkdir -p "$TEMP_BASE"
fi
TEMP_WORK_DIR=$(mktemp -d "$TEMP_BASE/ppa_build_work_XXXXXX")
# Cleanup function for temp directories
cleanup_temp_dirs() {
if [[ -z "${PPA_UPLOAD_SCRIPT:-}" ]] && [[ -d "${TEMP_WORK_DIR:-}" ]]; then
rm -rf "$TEMP_WORK_DIR"
fi
if [[ -d "${TEMP_CLONE:-}" ]]; then
rm -rf "$TEMP_CLONE"
fi
for temp_dir in "$TEMP_BASE"/ppa_clone_* "$TEMP_BASE"/ppa_tag_*; do
if [[ -d "$temp_dir" ]]; then
rm -rf "$temp_dir" 2>/dev/null || true
fi
done
}
trap cleanup_temp_dirs EXIT
info "Building source package for: $PACKAGE_NAME" info "Building source package for: $PACKAGE_NAME"
info "Package directory: $PACKAGE_DIR" info "Package directory: $PACKAGE_DIR"
info "Working directory: $TEMP_WORK_DIR" info "Working directory: $TEMP_WORK_DIR"
info "Target Ubuntu series: $UBUNTU_SERIES" info "Target Ubuntu series: $UBUNTU_SERIES"
# Check for required files
REQUIRED_FILES=( REQUIRED_FILES=(
"debian/control" "debian/control"
"debian/rules" "debian/rules"
@@ -87,14 +108,64 @@ fi
success "GPG key found" success "GPG key found"
# Check if debuild is installed # Function to get PPA name from package name
get_ppa_name() {
local pkg="$1"
case "$pkg" in
dms) echo "dms" ;;
dms-git) echo "dms-git" ;;
dms-greeter) echo "danklinux" ;;
*) echo "" ;;
esac
}
# Parameters:
# $1 = PPA_NAME
# $2 = SOURCE_NAME
# $3 = VERSION
# $4 = CHECK_MODE Exact version match, "commit" = check commit hash (default)
check_ppa_version_exists() {
local PPA_NAME="$1"
local SOURCE_NAME="$2"
local VERSION="$3"
local CHECK_MODE="${4:-commit}"
# Query Launchpad API
PPA_VERSION=$(curl -s \
"https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published" \
| grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
if [[ -n "$PPA_VERSION" ]]; then
# For git packages with "commit" mode, check if same commit already exists
if [[ "$CHECK_MODE" == "commit" ]] && [[ "$SOURCE_NAME" == *"-git" ]]; then
# Extract commit hash from versions (e.g., 79794d34 from 1.0.2+git2546.79794d34ppa2)
PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
NEW_COMMIT=$(echo "$VERSION" | grep -oP '\.[a-f0-9]{8}(ppa[0-9]+)?$' | grep -oP '[a-f0-9]{8}' || echo "")
if [[ -n "$PPA_COMMIT" && -n "$NEW_COMMIT" && "$PPA_COMMIT" == "$NEW_COMMIT" ]]; then
warn "Commit $NEW_COMMIT already exists in PPA (current version: $PPA_VERSION)"
return 0
fi
fi
# Exact version match check (always performed)
if [[ "$PPA_VERSION" == "$VERSION" ]]; then
warn "Version $VERSION already exists in PPA"
return 0
fi
else
warn "Could not fetch PPA version (API may be unavailable), proceeding anyway"
return 1
fi
return 1
}
if ! command -v debuild &>/dev/null; then if ! command -v debuild &>/dev/null; then
error "debuild not found. Install devscripts:" error "debuild not found. Install devscripts:"
error " sudo dnf install devscripts" error " sudo dnf install devscripts"
exit 1 exit 1
fi fi
# Extract package info from changelog
cd "$PACKAGE_DIR" cd "$PACKAGE_DIR"
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version) CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
SOURCE_NAME=$(dpkg-parsechangelog -S Source) SOURCE_NAME=$(dpkg-parsechangelog -S Source)
@@ -102,41 +173,24 @@ SOURCE_NAME=$(dpkg-parsechangelog -S Source)
info "Source package: $SOURCE_NAME" info "Source package: $SOURCE_NAME"
info "Version: $CHANGELOG_VERSION" info "Version: $CHANGELOG_VERSION"
# Check if version targets correct Ubuntu series
CHANGELOG_SERIES=$(dpkg-parsechangelog -S Distribution) CHANGELOG_SERIES=$(dpkg-parsechangelog -S Distribution)
if [ "$CHANGELOG_SERIES" != "$UBUNTU_SERIES" ] && [ "$CHANGELOG_SERIES" != "UNRELEASED" ]; then if [ "$CHANGELOG_SERIES" != "$UBUNTU_SERIES" ] && [ "$CHANGELOG_SERIES" != "UNRELEASED" ]; then
warn "Changelog targets '$CHANGELOG_SERIES' but building for '$UBUNTU_SERIES'" warn "Changelog targets '$CHANGELOG_SERIES' but building for '$UBUNTU_SERIES'"
warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES" warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES"
fi fi
# Check if this is a manual run or automated
IS_MANUAL=false
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
IS_MANUAL=true
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then
IS_MANUAL=true
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)"
elif [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" ]]; then
IS_MANUAL=true
echo "==> Manual workflow trigger detected (workflow_dispatch)"
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
IS_MANUAL=true
echo "==> Local/manual run detected (not in CI)"
fi
# Copy package to temp working directory
info "Copying package to working directory..." info "Copying package to working directory..."
cp -r "$PACKAGE_DIR" "$TEMP_WORK_DIR/" cp -r "$PACKAGE_DIR" "$TEMP_WORK_DIR/"
WORK_PACKAGE_DIR="$TEMP_WORK_DIR/$PACKAGE_NAME" WORK_PACKAGE_DIR="$TEMP_WORK_DIR/$PACKAGE_NAME"
# Detect package type and update version automatically if [ -f "$WORK_PACKAGE_DIR/debian/files" ]; then
cd "$WORK_PACKAGE_DIR" info "Removing old debian/files build artifact..."
rm -f "$WORK_PACKAGE_DIR/debian/files"
fi
# Function to get latest tag from GitHub cd "$WORK_PACKAGE_DIR"
get_latest_tag() { get_latest_tag() {
local repo="$1" local repo="$1"
# Try GitHub API first (faster)
if command -v curl &>/dev/null; then if command -v curl &>/dev/null; then
LATEST_TAG=$(curl -s "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null | grep '"tag_name":' | sed 's/.*"tag_name": "\(.*\)".*/\1/' | head -1) LATEST_TAG=$(curl -s "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null | grep '"tag_name":' | sed 's/.*"tag_name": "\(.*\)".*/\1/' | head -1)
if [ -n "$LATEST_TAG" ]; then if [ -n "$LATEST_TAG" ]; then
@@ -144,8 +198,7 @@ get_latest_tag() {
return return
fi fi
fi fi
# Fallback: clone and get latest tag TEMP_REPO=$(mktemp -d "$TEMP_BASE/ppa_tag_XXXXXX")
TEMP_REPO=$(mktemp -d)
if git clone --depth=1 --quiet "https://github.com/$repo.git" "$TEMP_REPO" 2>/dev/null; then if git clone --depth=1 --quiet "https://github.com/$repo.git" "$TEMP_REPO" 2>/dev/null; then
LATEST_TAG=$(cd "$TEMP_REPO" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "") LATEST_TAG=$(cd "$TEMP_REPO" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "")
rm -rf "$TEMP_REPO" rm -rf "$TEMP_REPO"
@@ -153,27 +206,21 @@ get_latest_tag() {
fi fi
} }
# Detect if package is git-based
IS_GIT_PACKAGE=false IS_GIT_PACKAGE=false
GIT_REPO="" GIT_REPO=""
SOURCE_DIR="" SOURCE_DIR=""
# Check package name for -git suffix
if [[ "$PACKAGE_NAME" == *"-git" ]]; then if [[ "$PACKAGE_NAME" == *"-git" ]]; then
IS_GIT_PACKAGE=true IS_GIT_PACKAGE=true
fi fi
# Check rules file for git clone patterns and extract repo
if grep -q "git clone" debian/rules 2>/dev/null; then if grep -q "git clone" debian/rules 2>/dev/null; then
IS_GIT_PACKAGE=true IS_GIT_PACKAGE=true
# Extract GitHub repo URL from rules
GIT_URL=$(grep -o "git clone.*https://github.com/[^/]*/[^/]*\.git" debian/rules 2>/dev/null | head -1 | sed 's/.*github\.com\///' | sed 's/\.git.*//' || echo "") GIT_URL=$(grep -o "git clone.*https://github.com/[^/]*/[^/]*\.git" debian/rules 2>/dev/null | head -1 | sed 's/.*github\.com\///' | sed 's/\.git.*//' || echo "")
if [ -n "$GIT_URL" ]; then if [ -n "$GIT_URL" ]; then
GIT_REPO="$GIT_URL" GIT_REPO="$GIT_URL"
fi fi
fi fi
# Special handling for known packages
case "$PACKAGE_NAME" in case "$PACKAGE_NAME" in
dms-git) dms-git)
IS_GIT_PACKAGE=true IS_GIT_PACKAGE=true
@@ -182,12 +229,101 @@ dms-git)
;; ;;
dms) dms)
GIT_REPO="AvengeMedia/DankMaterialShell" GIT_REPO="AvengeMedia/DankMaterialShell"
info "Downloading pre-built binaries and source for dms..." ;;
# Get version from changelog (remove ppa suffix for both quilt and native formats) dms-greeter)
# Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2 GIT_REPO="AvengeMedia/DankMaterialShell"
;;
danksearch)
GIT_REPO="AvengeMedia/danksearch"
;;
dgop)
GIT_REPO="AvengeMedia/dgop"
;;
esac
# Handle stable packages - update changelog FIRST before downloads
if [ "$IS_GIT_PACKAGE" = false ] && [ -n "$GIT_REPO" ]; then
info "Detected stable package: $PACKAGE_NAME"
info "Fetching latest tag from $GIT_REPO..."
LATEST_TAG=$(get_latest_tag "$GIT_REPO")
if [ -n "$LATEST_TAG" ]; then
SOURCE_FORMAT=$(head -1 debian/source/format 2>/dev/null || echo "3.0 (quilt)")
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
PPA_NUM=$REBUILD_RELEASE
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
else
PPA_NUM=1
fi
if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
BASE_VERSION="${LATEST_TAG}"
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
else
BASE_VERSION="${LATEST_TAG}-1"
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
fi
# Check if this version already exists in PPA (for stable packages, use exact match)
PPA_NAME=$(get_ppa_name "$PACKAGE_NAME")
if [[ -n "$PPA_NAME" ]]; then
info "Checking if version $NEW_VERSION already exists in PPA..."
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact"; then
error "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME"
error " To rebuild with a different release number, use:"
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
exit 1
fi
else
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME"
NEXT_NUM=$((REBUILD_RELEASE + 1))
error " To rebuild with a different release number, use:"
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME $NEXT_NUM"
exit 1
fi
fi
fi
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
if [ "$PPA_NUM" -gt 1 ]; then
info "Updating changelog for rebuild (PPA number incremented to $PPA_NUM)"
else
info "Updating changelog to latest tag: $LATEST_TAG"
fi
if [ "$PPA_NUM" -gt 1 ]; then
CHANGELOG_MSG="Rebuild for packaging fixes (ppa${PPA_NUM})"
else
CHANGELOG_MSG="Upstream release ${LATEST_TAG}"
fi
# Single changelog entry (full history available on Launchpad)
cat >debian/changelog <<EOF
${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* ${CHANGELOG_MSG}
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)
EOF
success "Version updated to $NEW_VERSION"
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
# Note: No longer writing back to repository (changelog stays as template)
else
info "Version already at latest tag: $LATEST_TAG"
fi
else
warn "Could not determine latest tag for $GIT_REPO, using existing version"
fi
# Download binaries/source using the updated version from changelog
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//') VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
# Download amd64 binary (will be included in source package) case "$PACKAGE_NAME" in
dms)
info "Downloading pre-built binaries and source for dms..."
if [ ! -f "dms-distropkg-amd64.gz" ]; then if [ ! -f "dms-distropkg-amd64.gz" ]; then
info "Downloading dms binary for amd64..." info "Downloading dms binary for amd64..."
if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-distropkg-amd64.gz"; then if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-distropkg-amd64.gz"; then
@@ -198,7 +334,6 @@ dms)
fi fi
fi fi
# Download source tarball for QML files
if [ ! -f "dms-source.tar.gz" ]; then if [ ! -f "dms-source.tar.gz" ]; then
info "Downloading dms source for QML files..." info "Downloading dms source for QML files..."
if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
@@ -209,11 +344,8 @@ dms)
fi fi
fi fi
;; ;;
dms-greeter) dms-greeter)
GIT_REPO="AvengeMedia/DankMaterialShell"
info "Downloading source for dms-greeter..." info "Downloading source for dms-greeter..."
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
if [ ! -f "dms-greeter-source.tar.gz" ]; then if [ ! -f "dms-greeter-source.tar.gz" ]; then
info "Downloading dms-greeter source..." info "Downloading dms-greeter source..."
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
@@ -224,23 +356,14 @@ dms-greeter)
fi fi
fi fi
;; ;;
danksearch) esac
# danksearch uses pre-built binary from releases fi
GIT_REPO="AvengeMedia/danksearch"
;;
dgop)
# dgop uses pre-built binary from releases
GIT_REPO="AvengeMedia/dgop"
;;
esac
# Handle git packages # Handle git packages
if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
info "Detected git package: $PACKAGE_NAME" info "Detected git package: $PACKAGE_NAME"
# Determine source directory name
if [ -z "$SOURCE_DIR" ]; then if [ -z "$SOURCE_DIR" ]; then
# Default: use package name without -git suffix + -source or -repo
BASE_NAME=$(echo "$PACKAGE_NAME" | sed 's/-git$//') BASE_NAME=$(echo "$PACKAGE_NAME" | sed 's/-git$//')
if [ -d "${BASE_NAME}-source" ] 2>/dev/null; then if [ -d "${BASE_NAME}-source" ] 2>/dev/null; then
SOURCE_DIR="${BASE_NAME}-source" SOURCE_DIR="${BASE_NAME}-source"
@@ -253,27 +376,18 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
fi fi
fi fi
# Always clone fresh source to get latest commit info
info "Cloning $GIT_REPO from GitHub (getting latest commit info)..." info "Cloning $GIT_REPO from GitHub (getting latest commit info)..."
TEMP_CLONE=$(mktemp -d) TEMP_CLONE=$(mktemp -d "$TEMP_BASE/ppa_clone_XXXXXX")
if git clone "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then if git clone "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
# Get git commit info from fresh clone
GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short HEAD) GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short HEAD)
GIT_COMMIT_COUNT=$(cd "$TEMP_CLONE" && git rev-list --count HEAD) GIT_COMMIT_COUNT=$(cd "$TEMP_CLONE" && git rev-list --count HEAD)
# Get upstream version from latest git tag (e.g., 0.2.1)
# Sort all tags by version and get the latest one (not just the one reachable from HEAD)
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l "v*" | sed 's/^v//' | sort -V | tail -1) UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l "v*" | sed 's/^v//' | sort -V | tail -1)
if [ -z "$UPSTREAM_VERSION" ]; then if [ -z "$UPSTREAM_VERSION" ]; then
# Fallback: try without v prefix
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -1) UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -1)
fi fi
if [ -z "$UPSTREAM_VERSION" ]; then if [ -z "$UPSTREAM_VERSION" ]; then
# Last resort: use git describe
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.1") UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.1")
fi fi
# Verify we got valid commit info
if [ -z "$GIT_COMMIT_COUNT" ] || [ "$GIT_COMMIT_COUNT" = "0" ]; then if [ -z "$GIT_COMMIT_COUNT" ] || [ "$GIT_COMMIT_COUNT" = "0" ]; then
error "Failed to get commit count from $GIT_REPO" error "Failed to get commit count from $GIT_REPO"
rm -rf "$TEMP_CLONE" rm -rf "$TEMP_CLONE"
@@ -288,82 +402,85 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
success "Got commit info: $GIT_COMMIT_COUNT ($GIT_COMMIT_HASH), upstream: $UPSTREAM_VERSION" success "Got commit info: $GIT_COMMIT_COUNT ($GIT_COMMIT_HASH), upstream: $UPSTREAM_VERSION"
# Update changelog with git commit info # Build base version (without ppa suffix yet)
info "Updating changelog with git commit info..."
# Format: 0.2.1+git705.fdbb86appa1
# Check if we're rebuilding the same commit (increment PPA number if so)
BASE_VERSION="${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}" BASE_VERSION="${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}"
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
# Use REBUILD_RELEASE if provided, otherwise auto-increment # EARLY VERSION CHECK
PPA_NAME=$(get_ppa_name "$PACKAGE_NAME")
if [[ -n "$PPA_NAME" ]]; then
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..."
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit"; then
error "==> Error: This commit is already uploaded to PPA"
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
error " To rebuild the same commit, specify a rebuild number:"
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 3"
error " Or with build script directly:"
error " REBUILD_RELEASE=2 ./distro/scripts/ppa-build.sh $PACKAGE_DIR"
error " Or push a new commit first, then run:"
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME"
rm -rf "$TEMP_CLONE"
exit 1
fi
PPA_NUM=1
info "Using PPA number $PPA_NUM"
else
PPA_NUM=$REBUILD_RELEASE
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
info "Checking if version $NEW_VERSION already exists in PPA..."
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
error "==> Error: Version $NEW_VERSION already exists in PPA"
error " This exact version (including ppa${PPA_NUM}) is already uploaded."
NEXT_NUM=$((PPA_NUM + 1))
error " To rebuild with a different release number, try incrementing:"
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME $NEXT_NUM"
error " Or with build script directly:"
error " REBUILD_RELEASE=$NEXT_NUM ./distro/scripts/ppa-build.sh $PACKAGE_DIR"
rm -rf "$TEMP_CLONE"
exit 1
fi
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
fi
else
# No PPA name found, use default
if [[ -n "${REBUILD_RELEASE:-}" ]]; then if [[ -n "${REBUILD_RELEASE:-}" ]]; then
PPA_NUM=$REBUILD_RELEASE PPA_NUM=$REBUILD_RELEASE
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number" info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
else else
PPA_NUM=1 PPA_NUM=1
info "Using PPA number $PPA_NUM"
# If current version matches the base version, increment PPA number
# Escape special regex characters in BASE_VERSION for pattern matching
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/+/\\+/g')
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
PPA_NUM=$((BASH_REMATCH[1] + 1))
if [[ "$IS_MANUAL" == true ]]; then
info "Detected rebuild of same commit (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
else
info "Detected rebuild of same commit (current: $CURRENT_VERSION). Not a manual run, skipping."
success "No changes needed (commit matches)."
exit 0
fi
else
info "New commit or first build, using PPA number $PPA_NUM"
fi fi
fi fi
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}" NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
info "Updating changelog with git commit info..."
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
# Use sed to update changelog (non-interactive, faster) # Single changelog entry (git snapshots don't need history)
# Get current changelog content - find the next package header line (starts with package name) cat >debian/changelog <<EOF
# Skip the first entry entirely by finding the second occurrence of the package name at start of line ${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
if [ -n "$OLD_ENTRY_START" ]; then
# Found second entry, use everything from there
CHANGELOG_CONTENT=$(tail -n +"$OLD_ENTRY_START" debian/changelog)
else
# No second entry found, changelog will only have new entry
CHANGELOG_CONTENT=""
fi
# Create new changelog entry with proper format
CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* Git snapshot (commit ${GIT_COMMIT_COUNT}: ${GIT_COMMIT_HASH}) * Git snapshot (commit ${GIT_COMMIT_COUNT}: ${GIT_COMMIT_HASH})
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)" -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)
EOF
# Write new changelog (new entry, blank line, then old entries)
echo "$CHANGELOG_ENTRY" >debian/changelog
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "" >>debian/changelog
echo "$CHANGELOG_CONTENT" >>debian/changelog
fi
success "Version updated to $NEW_VERSION" success "Version updated to $NEW_VERSION"
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
# Note: No longer writing back to repository (changelog stays as template)
# Now clone to source directory (without .git for inclusion in package)
rm -rf "$SOURCE_DIR" rm -rf "$SOURCE_DIR"
cp -r "$TEMP_CLONE" "$SOURCE_DIR" cp -r "$TEMP_CLONE" "$SOURCE_DIR"
# Save version info for dms-git build process
if [ "$PACKAGE_NAME" = "dms-git" ]; then if [ "$PACKAGE_NAME" = "dms-git" ]; then
info "Saving version info to .dms-version for build process..." info "Saving version info to .dms-version for build process..."
echo "VERSION=${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}" >"$SOURCE_DIR/.dms-version" echo "VERSION=${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}" >"$SOURCE_DIR/.dms-version"
echo "COMMIT=${GIT_COMMIT_HASH}" >>"$SOURCE_DIR/.dms-version" echo "COMMIT=${GIT_COMMIT_HASH}" >>"$SOURCE_DIR/.dms-version"
success "Version info saved: ${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}" success "Version info saved: ${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}"
# Vendor Go dependencies (Launchpad has no internet access)
info "Vendoring Go dependencies for offline build..." info "Vendoring Go dependencies for offline build..."
cd "$SOURCE_DIR/core" cd "$SOURCE_DIR/core"
# Create vendor directory with all dependencies
go mod vendor go mod vendor
if [ ! -d "vendor" ]; then if [ ! -d "vendor" ]; then
@@ -378,167 +495,21 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
rm -rf "$SOURCE_DIR/.git" rm -rf "$SOURCE_DIR/.git"
rm -rf "$TEMP_CLONE" rm -rf "$TEMP_CLONE"
# Vendor Rust dependencies for packages that need it
if false; then
# No current packages need Rust vendoring
if [ -f "$SOURCE_DIR/Cargo.toml" ]; then
info "Vendoring Rust dependencies (Launchpad has no internet access)..."
cd "$SOURCE_DIR"
# Clean up any existing vendor directory and .orig files
# (prevents cargo from including .orig files in checksums)
rm -rf vendor .cargo
find . -type f -name "*.orig" -exec rm -f {} + || true
# Download all dependencies (crates.io + git repos) to vendor/
# cargo vendor outputs the config to stderr, capture it
mkdir -p .cargo
cargo vendor 2>&1 | awk '
/^\[source\.crates-io\]/ { printing=1 }
printing { print }
/^directory = "vendor"$/ { exit }
' >.cargo/config.toml
# Verify vendor directory was created
if [ ! -d "vendor" ]; then
error "Failed to vendor dependencies"
exit 1
fi
# Verify config was created
if [ ! -s .cargo/config.toml ]; then
error "Failed to create cargo config"
exit 1
fi
# CRITICAL: Remove ALL .orig files from vendor directory
# These break cargo checksums when dh_clean tries to use them
info "Cleaning .orig files from vendor directory..."
find vendor -type f -name "*.orig" -exec rm -fv {} + || true
find vendor -type f -name "*.rej" -exec rm -fv {} + || true
# Verify no .orig files remain
ORIG_COUNT=$(find vendor -type f -name "*.orig" | wc -l)
if [ "$ORIG_COUNT" -gt 0 ]; then
warn "Found $ORIG_COUNT .orig files still in vendor directory"
fi
success "Rust dependencies vendored (including git dependencies)"
cd "$PACKAGE_DIR"
fi
fi
success "Source prepared for packaging" success "Source prepared for packaging"
else else
error "Failed to clone $GIT_REPO" error "Failed to clone $GIT_REPO"
rm -rf "$TEMP_CLONE" rm -rf "$TEMP_CLONE"
exit 1 exit 1
fi fi
# Handle stable packages - get latest tag
elif [ -n "$GIT_REPO" ]; then
info "Detected stable package: $PACKAGE_NAME"
info "Fetching latest tag from $GIT_REPO..."
LATEST_TAG=$(get_latest_tag "$GIT_REPO")
if [ -n "$LATEST_TAG" ]; then
# Check source format - native packages can't use dashes
SOURCE_FORMAT=$(head -1 debian/source/format 2>/dev/null || echo "3.0 (quilt)")
# Get current version to check if we need to increment PPA number
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
# Use REBUILD_RELEASE if provided, otherwise auto-increment
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
PPA_NUM=$REBUILD_RELEASE
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
else
PPA_NUM=1
fi
if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
# Native format: 0.2.1ppa1 (no dash, no revision)
BASE_VERSION="${LATEST_TAG}"
# Check if we're rebuilding the same version (increment PPA number if so)
if [[ -z "${REBUILD_RELEASE:-}" ]] && [[ "$CURRENT_VERSION" =~ ^${LATEST_TAG}ppa([0-9]+)$ ]]; then
PPA_NUM=$((BASH_REMATCH[1] + 1))
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
elif [[ -z "${REBUILD_RELEASE:-}" ]]; then
info "New version or first build, using PPA number $PPA_NUM"
fi
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
else
# Quilt format: 0.2.1-1ppa1 (with revision)
BASE_VERSION="${LATEST_TAG}-1"
# Check if we're rebuilding the same version (increment PPA number if so)
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/-/\\-/g')
if [[ -z "${REBUILD_RELEASE:-}" ]] && [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
PPA_NUM=$((BASH_REMATCH[1] + 1))
if [[ "$IS_MANUAL" == true ]]; then
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
else
info "Detected rebuild of same version (current: $CURRENT_VERSION). Not a manual run, skipping."
success "No changes needed (version matches)."
exit 0
fi
elif [[ -z "${REBUILD_RELEASE:-}" ]]; then
info "New version or first build, using PPA number $PPA_NUM"
fi
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
fi
# Check if version needs updating (either new version or PPA number changed)
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
if [ "$PPA_NUM" -gt 1 ]; then
info "Updating changelog for rebuild (PPA number incremented to $PPA_NUM)"
else
info "Updating changelog to latest tag: $LATEST_TAG"
fi
# Use sed to update changelog (non-interactive)
# Get current changelog content - find the next package header line
OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
if [ -n "$OLD_ENTRY_START" ]; then
CHANGELOG_CONTENT=$(tail -n +"$OLD_ENTRY_START" debian/changelog)
else
CHANGELOG_CONTENT=""
fi
# Create appropriate changelog message
if [ "$PPA_NUM" -gt 1 ]; then
CHANGELOG_MSG="Rebuild for packaging fixes (ppa${PPA_NUM})"
else
CHANGELOG_MSG="Upstream release ${LATEST_TAG}"
fi
CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* ${CHANGELOG_MSG}
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
echo "$CHANGELOG_ENTRY" >debian/changelog
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "" >>debian/changelog
echo "$CHANGELOG_CONTENT" >>debian/changelog
fi
success "Version updated to $NEW_VERSION"
else
info "Version already at latest tag: $LATEST_TAG"
fi
else
warn "Could not determine latest tag for $GIT_REPO, using existing version"
fi
fi fi
# Handle packages that need pre-built binaries downloaded # Handle packages that need pre-built binaries downloaded
cd "$PACKAGE_DIR" cd "$WORK_PACKAGE_DIR"
case "$PACKAGE_NAME" in case "$PACKAGE_NAME" in
danksearch) danksearch)
info "Downloading pre-built binaries for danksearch..." info "Downloading pre-built binaries for danksearch..."
# Get version from changelog (remove ppa suffix for both quilt and native formats)
# Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//') VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
# Download both amd64 and arm64 binaries (will be included in source package)
# Launchpad can't download during build, so we include both architectures
if [ ! -f "dsearch-amd64" ]; then if [ ! -f "dsearch-amd64" ]; then
info "Downloading dsearch binary for amd64..." info "Downloading dsearch binary for amd64..."
if wget -O dsearch-amd64.gz "https://github.com/AvengeMedia/danksearch/releases/download/v${VERSION}/dsearch-linux-amd64.gz"; then if wget -O dsearch-amd64.gz "https://github.com/AvengeMedia/danksearch/releases/download/v${VERSION}/dsearch-linux-amd64.gz"; then
@@ -564,46 +535,26 @@ danksearch)
fi fi
;; ;;
dgop) dgop)
# dgop binary should already be committed in the repo
if [ ! -f "dgop" ]; then if [ ! -f "dgop" ]; then
warn "dgop binary not found - should be committed to repo" warn "dgop binary not found - should be committed to repo"
fi fi
;; ;;
esac esac
cd - >/dev/null cd "$WORK_PACKAGE_DIR"
# Check if this version already exists on PPA (only in CI environment)
if command -v rmadison >/dev/null 2>&1; then
info "Checking if version already exists on PPA..."
PPA_VERSION_CHECK=$(rmadison -u ppa:avengemedia/dms "$PACKAGE_NAME" 2>/dev/null | grep "$VERSION" || true)
if [ -n "$PPA_VERSION_CHECK" ]; then
warn "Version $VERSION already exists on PPA:"
echo "$PPA_VERSION_CHECK"
echo
warn "Skipping upload to avoid duplicate. If this is a rebuild, increment the ppa number."
cd "$PACKAGE_DIR"
# Still clean up extracted sources
case "$PACKAGE_NAME" in
dms-git)
rm -rf DankMaterialShell-*
success "Cleaned up DankMaterialShell-*/ directory"
;;
esac
exit 0
fi
fi
# Build source package
info "Building source package..." info "Building source package..."
echo echo
# Determine if we need to include orig tarball (-sa) or just debian changes (-sd) SOURCE_FORMAT=$(head -1 "$WORK_PACKAGE_DIR/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
# Check if .orig.tar.xz already exists in real parent directory (previous build)
ORIG_TARBALL="${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz" # Native format packages don't use orig tarballs - they include everything in one tarball
if [ -f "$PACKAGE_PARENT/$ORIG_TARBALL" ]; then if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
info "Native format detected - including all source files (no orig tarball needed)"
DEBUILD_SOURCE_FLAG="-sa"
elif [ -f "$PACKAGE_PARENT/${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz" ]; then
ORIG_TARBALL="${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz"
info "Found existing orig tarball in $PACKAGE_PARENT, using -sd (debian changes only)" info "Found existing orig tarball in $PACKAGE_PARENT, using -sd (debian changes only)"
# Copy it to temp parent so debuild can find it
cp "$PACKAGE_PARENT/$ORIG_TARBALL" "$TEMP_WORK_DIR/" cp "$PACKAGE_PARENT/$ORIG_TARBALL" "$TEMP_WORK_DIR/"
DEBUILD_SOURCE_FLAG="-sd" DEBUILD_SOURCE_FLAG="-sd"
else else
@@ -611,20 +562,20 @@ else
DEBUILD_SOURCE_FLAG="-sa" DEBUILD_SOURCE_FLAG="-sa"
fi fi
# Use -S for source only, -sa/-sd for source inclusion
# -d skips dependency checking (we're building on Fedora, not Ubuntu) # -d skips dependency checking (we're building on Fedora, not Ubuntu)
# Pipe yes to automatically answer prompts (e.g., "continue anyway?")
if yes | DEBIAN_FRONTEND=noninteractive debuild -S $DEBUILD_SOURCE_FLAG -d; then if yes | DEBIAN_FRONTEND=noninteractive debuild -S $DEBUILD_SOURCE_FLAG -d; then
echo echo
success "Source package built successfully!" success "Source package built successfully!"
# Copy build artifacts back to parent directory TEMP_MARKER_FILE="$PACKAGE_PARENT/.ppa_build_temp_${PACKAGE_NAME}"
info "Copying build artifacts to $PACKAGE_PARENT..." echo "PPA_BUILD_TEMP_DIR=$TEMP_WORK_DIR" > "$TEMP_MARKER_FILE"
cp -v "$TEMP_WORK_DIR"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* "$PACKAGE_PARENT/" 2>/dev/null || true
# List generated files if [[ -z "${PPA_UPLOAD_SCRIPT:-}" ]] && ! pgrep -f "ppa-upload.sh" >/dev/null 2>&1; then
info "Copying build artifacts to $PACKAGE_PARENT (standalone build)..."
cp -v "$TEMP_WORK_DIR"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* "$PACKAGE_PARENT/" 2>/dev/null || true
info "Generated files in $PACKAGE_PARENT:" info "Generated files in $PACKAGE_PARENT:"
ls -lh "$PACKAGE_PARENT"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* 2>/dev/null || true ls -lh "$PACKAGE_PARENT"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* 2>/dev/null || true
fi
# Show what to do next # Show what to do next
echo echo
-204
View File
@@ -1,204 +0,0 @@
#!/bin/bash
# Ubuntu PPA uploader for DMS packages
# Usage: ./upload-ppa.sh <changes-file> <ppa-name>
#
# Example:
# ./upload-ppa.sh ../dms_0.5.2ppa1_source.changes dms
# ./upload-ppa.sh ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
if [ $# -lt 2 ]; then
error "Usage: $0 <changes-file> <ppa-name>"
echo
echo "Arguments:"
echo " changes-file : Path to .changes file (e.g., ../dms_0.5.2ppa1_source.changes)"
echo " ppa-name : PPA to upload to (dms or dms-git)"
echo
echo "Examples:"
echo " $0 ../dms_0.5.2ppa1_source.changes dms"
echo " $0 ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git"
exit 1
fi
CHANGES_FILE="$1"
PPA_NAME="$2"
# Validate changes file
if [ ! -f "$CHANGES_FILE" ]; then
error "Changes file not found: $CHANGES_FILE"
exit 1
fi
if [[ ! "$CHANGES_FILE" =~ \.changes$ ]]; then
error "File must be a .changes file"
exit 1
fi
# Validate PPA name
if [ "$PPA_NAME" != "dms" ] && [ "$PPA_NAME" != "dms-git" ] && [ "$PPA_NAME" != "danklinux" ]; then
error "PPA name must be 'dms', 'dms-git', or 'danklinux'"
exit 1
fi
# Get absolute path
CHANGES_FILE=$(realpath "$CHANGES_FILE")
info "Uploading to PPA: ppa:avengemedia/$PPA_NAME"
info "Changes file: $CHANGES_FILE"
# Check if dput is installed
if command -v dput &>/dev/null; then
info "dput found"
else
error "dput not found. Install with:"
error " sudo dnf install dput-ng"
exit 1
fi
# Check if ~/.dput.cf exists
if [ ! -f "$HOME/.dput.cf" ]; then
error "$HOME/.dput.cf not found!"
echo
info "Create it from template:"
echo " cp $(dirname "$0")/../dput.cf.template ~/.dput.cf"
echo
info "Or create it manually with:"
cat <<'EOF'
[ppa:avengemedia/dms]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/dms/
login = anonymous
allow_unsigned_uploads = 0
[ppa:avengemedia/dms-git]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/dms-git/
login = anonymous
allow_unsigned_uploads = 0
EOF
exit 1
fi
# Check if PPA is configured in dput.cf
if ! grep -q "^\[ppa:avengemedia/$PPA_NAME\]" "$HOME/.dput.cf"; then
error "PPA 'ppa:avengemedia/$PPA_NAME' not found in ~/.dput.cf"
echo
info "Add this to ~/.dput.cf:"
cat <<EOF
[ppa:avengemedia/$PPA_NAME]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/$PPA_NAME/
login = anonymous
allow_unsigned_uploads = 0
EOF
exit 1
fi
# Extract package info from changes file
PACKAGE_NAME=$(grep "^Source:" "$CHANGES_FILE" | awk '{print $2}')
VERSION=$(grep "^Version:" "$CHANGES_FILE" | awk '{print $2}')
info "Package: $PACKAGE_NAME"
info "Version: $VERSION"
# Show files that will be uploaded
echo
info "Files to be uploaded:"
grep "^ [a-f0-9]" "$CHANGES_FILE" | awk '{print " - " $5}' || true
# Verify GPG signature
info "Verifying GPG signature..."
if gpg --verify "$CHANGES_FILE" 2>/dev/null; then
success "GPG signature valid"
else
error "GPG signature verification failed!"
error "The .changes file must be signed with your GPG key"
exit 1
fi
# Ask for confirmation
echo
warn "About to upload to: ppa:avengemedia/$PPA_NAME"
read -p "Continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
info "Upload cancelled"
exit 0
fi
# Upload to PPA
info "Uploading to Launchpad..."
echo
UPLOAD_SUCCESS=false
if [ "$UPLOAD_METHOD" = "dput" ]; then
if dput "ppa:avengemedia/$PPA_NAME" "$CHANGES_FILE"; then
UPLOAD_SUCCESS=true
fi
elif [ "$UPLOAD_METHOD" = "lftp" ]; then
# Use lftp to upload to Launchpad PPA
CHANGES_DIR=$(dirname "$CHANGES_FILE")
CHANGES_BASENAME=$(basename "$CHANGES_FILE")
# Extract files to upload from .changes file
FILES_TO_UPLOAD=("$CHANGES_BASENAME")
while IFS= read -r line; do
if [[ "$line" =~ ^\ [a-f0-9]+\ [0-9]+\ [^\ ]+\ [^\ ]+\ (.+)$ ]]; then
FILES_TO_UPLOAD+=("${BASH_REMATCH[1]}")
fi
done < "$CHANGES_FILE"
# Build lftp command to upload all files
LFTP_COMMANDS="set ftp:ssl-allow no; open ftp://ppa.launchpad.net; user anonymous ''; cd ~avengemedia/ubuntu/$PPA_NAME/;"
for file in "${FILES_TO_UPLOAD[@]}"; do
LFTP_COMMANDS="$LFTP_COMMANDS put '$CHANGES_DIR/$file';"
done
LFTP_COMMANDS="$LFTP_COMMANDS bye"
if echo "$LFTP_COMMANDS" | lftp; then
UPLOAD_SUCCESS=true
fi
fi
if [ "$UPLOAD_SUCCESS" = true ]; then
echo
success "Upload successful!"
echo
info "Monitor build progress at:"
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
echo
info "Builds typically take 5-30 minutes depending on:"
echo " - Build queue length"
echo " - Package complexity"
echo " - Number of target Ubuntu series"
echo
info "Once built, users can install with:"
echo " sudo add-apt-repository ppa:avengemedia/$PPA_NAME"
echo " sudo apt update"
echo " sudo apt install $PACKAGE_NAME"
else
error "Upload failed!"
echo
info "Common issues:"
echo " - GPG key not verified on Launchpad (check https://launchpad.net/~/+editpgpkeys)"
echo " - Version already uploaded (must increment version number)"
echo " - Network/firewall blocking FTP (try HTTPS method in dput.cf)"
echo " - Email in changelog doesn't match GPG key email"
exit 1
fi
+217
View File
@@ -0,0 +1,217 @@
#!/bin/bash
# Unified PPA status checker for DMS packages
# Checks build status for packages across multiple PPAs via Launchpad API
# Usage: ./distro/scripts/ppa-status.sh [package-name] [ppa-name]
#
# Examples:
# ./distro/scripts/ppa-status.sh # Check all packages in all PPAs
# ./distro/scripts/ppa-status.sh dms # Check dms package
# ./distro/scripts/ppa-status.sh all dms-git # Check all packages in dms-git PPA
PPA_OWNER="avengemedia"
LAUNCHPAD_API="https://api.launchpad.net/1.0"
DISTRO_SERIES="questing"
# Define packages (sync with ppa-upload.sh)
ALL_PACKAGES=(dms dms-git dms-greeter)
# Function to get PPA name for a package
get_ppa_name() {
local pkg="$1"
case "$pkg" in
dms) echo "dms" ;;
dms-git) echo "dms-git" ;;
dms-greeter) echo "danklinux" ;;
*) echo "" ;;
esac
}
# Check for required tools
if ! command -v curl &> /dev/null; then
echo "Error: curl is required but not installed"
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed"
exit 1
fi
# Parse arguments
PACKAGE_INPUT="${1:-}"
PPA_INPUT="${2:-}"
# Determine packages and PPAs to check
if [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" != "all" ]]; then
# Check specific package
VALID_PACKAGE=false
for pkg in "${ALL_PACKAGES[@]}"; do
if [[ "$PACKAGE_INPUT" == "$pkg" ]]; then
VALID_PACKAGE=true
break
fi
done
if [[ "$VALID_PACKAGE" != "true" ]]; then
echo "Error: Unknown package: $PACKAGE_INPUT"
echo "Available packages: ${ALL_PACKAGES[*]}"
exit 1
fi
PACKAGES=("$PACKAGE_INPUT")
if [[ -n "$PPA_INPUT" ]]; then
PPAS=("$PPA_INPUT")
else
PPAS=("$(get_ppa_name "$PACKAGE_INPUT")")
fi
elif [[ -n "$PPA_INPUT" ]]; then
# Check all packages in specific PPA
PACKAGES=("${ALL_PACKAGES[@]}")
PPAS=("$PPA_INPUT")
else
# Check all packages in all PPAs
PACKAGES=("${ALL_PACKAGES[@]}")
PPAS=("dms" "dms-git" "danklinux")
fi
# Function to get build status color and symbol
get_status_display() {
local status="$1"
case "$status" in
"Successfully built")
echo -e "✅ \033[0;32m$status\033[0m"
;;
"Failed to build")
echo -e "❌ \033[0;31m$status\033[0m"
;;
"Needs building"|"Currently building")
echo -e "⏳ \033[0;33m$status\033[0m"
;;
"Dependency wait")
echo -e "⚠️ \033[0;33m$status\033[0m"
;;
"Chroot problem")
echo -e "🔧 \033[0;31m$status\033[0m"
;;
"Uploading build")
echo -e "📤 \033[0;36m$status\033[0m"
;;
*)
echo -e "❓ \033[0;37m$status\033[0m"
;;
esac
}
# Check each PPA
for PPA_NAME in "${PPAS[@]}"; do
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
echo "=========================================="
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} ==="
echo "=========================================="
echo "Distribution: Ubuntu $DISTRO_SERIES"
echo ""
for pkg in "${PACKAGES[@]}"; do
# Only check packages that belong to this PPA
PKG_PPA=$(get_ppa_name "$pkg")
if [[ "$PKG_PPA" != "$PPA_NAME" ]]; then
continue
fi
echo "----------------------------------------"
echo "--- $pkg ---"
echo "----------------------------------------"
# Get published sources for this package
SOURCES_URL="${PPA_ARCHIVE}?ws.op=getPublishedSources&source_name=${pkg}&distro_series=${LAUNCHPAD_API}/ubuntu/${DISTRO_SERIES}&status=Published"
SOURCES=$(curl -s "$SOURCES_URL" 2>/dev/null)
if [[ -z "$SOURCES" ]] || [[ "$SOURCES" == "null" ]]; then
echo " ⚠️ No published sources found"
echo ""
continue
fi
# Get the latest source
TOTAL=$(echo "$SOURCES" | jq '.total_size // 0')
if [[ "$TOTAL" == "0" ]]; then
echo " ⚠️ No published sources found for $DISTRO_SERIES"
echo ""
continue
fi
# Get most recent entry
ENTRY=$(echo "$SOURCES" | jq '.entries[0]')
if [[ "$ENTRY" == "null" ]]; then
echo " ⚠️ No source entries found"
echo ""
continue
fi
# Extract source info
VERSION=$(echo "$ENTRY" | jq -r '.source_package_version // "unknown"')
STATUS=$(echo "$ENTRY" | jq -r '.status // "unknown"')
DATE_PUBLISHED=$(echo "$ENTRY" | jq -r '.date_published // "unknown"')
SELF_LINK=$(echo "$ENTRY" | jq -r '.self_link // ""')
echo " 📦 Version: $VERSION"
echo " 📅 Published: ${DATE_PUBLISHED%T*}"
echo " 📋 Source Status: $STATUS"
echo ""
# Get builds for this source
if [[ -n "$SELF_LINK" && "$SELF_LINK" != "null" ]]; then
BUILDS_URL="${SELF_LINK}?ws.op=getBuilds"
BUILDS=$(curl -s "$BUILDS_URL" 2>/dev/null)
if [[ -n "$BUILDS" && "$BUILDS" != "null" ]]; then
BUILD_COUNT=$(echo "$BUILDS" | jq '.total_size // 0')
if [[ "$BUILD_COUNT" -gt 0 ]]; then
echo " Builds:"
echo "$BUILDS" | jq -r '.entries[] | "\(.arch_tag) \(.buildstate)"' 2>/dev/null | while read -r line; do
ARCH=$(echo "$line" | awk '{print $1}')
BUILD_STATUS=$(echo "$line" | cut -d' ' -f2-)
DISPLAY=$(get_status_display "$BUILD_STATUS")
echo " $ARCH: $DISPLAY"
done
fi
fi
fi
# Alternative: Get build records directly from archive
BUILD_RECORDS_URL="${PPA_ARCHIVE}?ws.op=getBuildRecords&source_name=${pkg}"
BUILD_RECORDS=$(curl -s "$BUILD_RECORDS_URL" 2>/dev/null)
if [[ -n "$BUILD_RECORDS" && "$BUILD_RECORDS" != "null" ]]; then
RECORD_COUNT=$(echo "$BUILD_RECORDS" | jq '.total_size // 0')
if [[ "$RECORD_COUNT" -gt 0 ]]; then
echo ""
echo " Recent build history:"
# Get unique version+arch combinations
echo "$BUILD_RECORDS" | jq -r '.entries[:6][] | "\(.source_package_version) \(.arch_tag) \(.buildstate)"' 2>/dev/null | while read -r line; do
VER=$(echo "$line" | awk '{print $1}')
ARCH=$(echo "$line" | awk '{print $2}')
BUILD_STATUS=$(echo "$line" | cut -d' ' -f3-)
DISPLAY=$(get_status_display "$BUILD_STATUS")
echo " $VER ($ARCH): $DISPLAY"
done
fi
fi
echo ""
done
echo "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
echo ""
done
echo "=========================================="
echo "Status check complete!"
echo ""
+187 -55
View File
@@ -1,10 +1,16 @@
#!/bin/bash #!/bin/bash
# Build and upload PPA package with automatic cleanup # Build and upload PPA package with automatic cleanup
# Usage: ./create-and-upload.sh <package-dir> <ppa-name> [ubuntu-series] [--keep-builds] # Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
# #
# Example: # Examples:
# ./create-and-upload.sh ../dms dms questing # ./ppa-upload.sh dms # Single package (auto-detects PPA)
# ./create-and-upload.sh ../danklinux/dgop danklinux questing --keep-builds # ./ppa-upload.sh dms 2 # Rebuild with ppa2 (simple syntax)
# ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax)
# ./ppa-upload.sh dms-git # Single package
# ./ppa-upload.sh all # All packages
# ./ppa-upload.sh dms dms questing # Explicit PPA and series
# ./ppa-upload.sh dms dms questing 2 # Explicit PPA, series, and rebuild number
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
set -e set -e
@@ -19,72 +25,204 @@ success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Parse arguments AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
KEEP_BUILDS=false KEEP_BUILDS=false
ARGS=() REBUILD_RELEASE=""
POSITIONAL_ARGS=()
for arg in "$@"; do for arg in "$@"; do
if [ "$arg" = "--keep-builds" ]; then case "$arg" in
KEEP_BUILDS=true --keep-builds) KEEP_BUILDS=true ;;
--rebuild=*)
REBUILD_RELEASE="${arg#*=}"
;;
-r|--rebuild)
REBUILD_NEXT=true
;;
*)
if [[ -n "${REBUILD_NEXT:-}" ]]; then
REBUILD_RELEASE="$arg"
REBUILD_NEXT=false
else else
ARGS+=("$arg") POSITIONAL_ARGS+=("$arg")
fi fi
;;
esac
done done
if [ ${#ARGS[@]} -lt 2 ]; then PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
error "Usage: $0 <package-dir> <ppa-name> [ubuntu-series] [--keep-builds]" PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
echo UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
echo "Arguments:"
echo " package-dir : Path to package directory (e.g., ../dms, ../danklinux/dgop)" if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
echo " ppa-name : PPA name (danklinux, dms, dms-git)" LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
echo " ubuntu-series : Ubuntu series (optional, default: questing)" LAST_ARG="${POSITIONAL_ARGS[$LAST_INDEX]}"
echo " Supported: questing (25.10) and newer only" if [[ "$LAST_ARG" =~ ^[0-9]+$ ]] && [[ -z "$REBUILD_RELEASE" ]]; then
echo " Note: Requires Qt 6.6+ (quickshell requirement)" # Last argument is a number and no --rebuild flag was used
echo " --keep-builds : Keep build artifacts after upload (optional)" # Use it as rebuild release and remove from positional args
echo REBUILD_RELEASE="$LAST_ARG"
echo "Examples:" POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
echo " $0 ../dms dms questing" PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
echo " $0 ../danklinux/dgop danklinux questing --keep-builds" PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
echo " $0 ../dms-git dms-git # Defaults to questing" UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
exit 1 fi
fi fi
PACKAGE_DIR="${ARGS[0]}"
PPA_NAME="${ARGS[1]}"
UBUNTU_SERIES="${ARGS[2]:-questing}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh" BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
UPLOAD_SCRIPT="$SCRIPT_DIR/ppa-dput.sh"
# Validate scripts exist
if [ ! -f "$BUILD_SCRIPT" ]; then if [ ! -f "$BUILD_SCRIPT" ]; then
error "Build script not found: $BUILD_SCRIPT" error "Build script not found: $BUILD_SCRIPT"
exit 1 exit 1
fi fi
# Get absolute path get_ppa_name() {
local pkg="$1"
case "$pkg" in
dms) echo "dms" ;;
dms-git) echo "dms-git" ;;
dms-greeter) echo "danklinux" ;;
*) echo "" ;;
esac
}
# Support both path-style and name-style arguments
PACKAGE_DIR=""
PACKAGE_NAME=""
PPA_NAME=""
if [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == *"/"* ]]; then
# Path-style argument (backward compatibility)
if [[ -d "$PACKAGE_INPUT" ]]; then
PACKAGE_DIR="$(cd "$PACKAGE_INPUT" && pwd)"
elif [[ -d "$REPO_ROOT/$PACKAGE_INPUT" ]]; then
PACKAGE_DIR="$(cd "$REPO_ROOT/$PACKAGE_INPUT" && pwd)"
else
error "Package directory not found: $PACKAGE_INPUT"
exit 1
fi
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
PPA_NAME="${PPA_NAME_INPUT:-$(get_ppa_name "$PACKAGE_NAME")}"
if [[ -z "$PPA_NAME" ]]; then
error "Could not determine PPA name for package: $PACKAGE_NAME"
error "Please specify PPA name as second argument"
exit 1
fi
info "Using path-style argument: $PACKAGE_DIR"
elif [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == "all" ]]; then
echo ""
info "Building and uploading all packages..."
FAILED_PACKAGES=()
for pkg in "${AVAILABLE_PACKAGES[@]}"; do
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
info "Processing $pkg..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
BUILD_ARGS=("$pkg" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
if ! "$0" "${BUILD_ARGS[@]}"; then
FAILED_PACKAGES+=("$pkg")
error "$pkg failed to upload"
fi
done
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [[ ${#FAILED_PACKAGES[@]} -eq 0 ]]; then
success "All packages uploaded successfully!"
else
error "Some packages failed: ${FAILED_PACKAGES[*]}"
exit 1
fi
exit 0
elif [[ -n "$PACKAGE_INPUT" ]]; then
VALID_PACKAGE=false
for pkg in "${AVAILABLE_PACKAGES[@]}"; do
if [[ "$PACKAGE_INPUT" == "$pkg" ]]; then
VALID_PACKAGE=true
break
fi
done
if [[ "$VALID_PACKAGE" != "true" ]]; then
error "Unknown package: $PACKAGE_INPUT"
echo "Available packages: ${AVAILABLE_PACKAGES[*]}"
exit 1
fi
PACKAGE_NAME="$PACKAGE_INPUT"
PACKAGE_DIR="$REPO_ROOT/distro/ubuntu/$PACKAGE_NAME"
PPA_NAME="${PPA_NAME_INPUT:-$(get_ppa_name "$PACKAGE_NAME")}"
else
echo "Available packages:"
echo ""
for i in "${!AVAILABLE_PACKAGES[@]}"; do
echo " $((i+1)). ${AVAILABLE_PACKAGES[$i]}"
done
echo " a. all"
echo ""
read -rp "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
PACKAGE_INPUT="all"
BUILD_ARGS=("all" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
exec "$0" "${BUILD_ARGS[@]}"
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
PACKAGE_NAME="${AVAILABLE_PACKAGES[$((selection-1))]}"
PACKAGE_DIR="$REPO_ROOT/distro/ubuntu/$PACKAGE_NAME"
PPA_NAME="${PPA_NAME_INPUT:-$(get_ppa_name "$PACKAGE_NAME")}"
else
error "Invalid selection"
exit 1
fi
fi
if [ ! -d "$PACKAGE_DIR" ]; then
error "Package directory not found: $PACKAGE_DIR"
exit 1
fi
if [ ! -d "$PACKAGE_DIR/debian" ]; then
error "No debian/ directory found in $PACKAGE_DIR"
exit 1
fi
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd) PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
PARENT_DIR=$(dirname "$PACKAGE_DIR") PARENT_DIR=$(dirname "$PACKAGE_DIR")
info "Building and uploading: $PACKAGE_NAME" info "Building and uploading: $PACKAGE_NAME"
info "Package directory: $PACKAGE_DIR" info "Package directory: $PACKAGE_DIR"
info "PPA: ppa:avengemedia/$PPA_NAME" info "PPA: ppa:avengemedia/$PPA_NAME"
info "Ubuntu series: $UBUNTU_SERIES" info "Ubuntu series: $UBUNTU_SERIES"
if [[ -n "$REBUILD_RELEASE" ]]; then
info "Rebuild release number: ppa$REBUILD_RELEASE"
fi
echo echo
# Step 1: Build source package
info "Step 1: Building source package..." info "Step 1: Building source package..."
if [[ -n "$REBUILD_RELEASE" ]]; then
export REBUILD_RELEASE
fi
export PPA_UPLOAD_SCRIPT=1
if ! "$BUILD_SCRIPT" "$PACKAGE_DIR" "$UBUNTU_SERIES"; then if ! "$BUILD_SCRIPT" "$PACKAGE_DIR" "$UBUNTU_SERIES"; then
error "Build failed!" error "Build failed!"
exit 1 exit 1
fi fi
# Find the changes file TEMP_DIR_FILE="$PARENT_DIR/.ppa_build_temp_${PACKAGE_NAME}"
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1) if [ -f "$TEMP_DIR_FILE" ]; then
BUILD_TEMP_DIR=$(grep -oP 'PPA_BUILD_TEMP_DIR=\K.*' "$TEMP_DIR_FILE")
rm -f "$TEMP_DIR_FILE"
info "Using build artifacts from temp directory: $BUILD_TEMP_DIR"
CHANGES_FILE=$(find "$BUILD_TEMP_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f 2>/dev/null | sort -V | tail -1)
else
BUILD_TEMP_DIR="$PARENT_DIR"
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
fi
if [ -z "$CHANGES_FILE" ]; then if [ -z "$CHANGES_FILE" ]; then
warn "Changes file not found in $PARENT_DIR" warn "Changes file not found in $BUILD_TEMP_DIR"
warn "Assuming build was skipped (no changes needed) and exiting successfully." warn "Assuming build was skipped (no changes needed) and exiting successfully."
exit 0 exit 0
fi fi
@@ -92,14 +230,11 @@ fi
info "Found changes file: $CHANGES_FILE" info "Found changes file: $CHANGES_FILE"
echo echo
# Step 2: Upload to PPA
info "Step 2: Uploading to PPA..." info "Step 2: Uploading to PPA..."
# Check if using lftp (for all PPAs) or dput
if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
warn "Using lftp for upload" warn "Using lftp for upload"
# Find all files to upload
BUILD_DIR=$(dirname "$CHANGES_FILE") BUILD_DIR=$(dirname "$CHANGES_FILE")
CHANGES_BASENAME=$(basename "$CHANGES_FILE") CHANGES_BASENAME=$(basename "$CHANGES_FILE")
DSC_FILE="${CHANGES_BASENAME/_source.changes/.dsc}" DSC_FILE="${CHANGES_BASENAME/_source.changes/.dsc}"
@@ -127,7 +262,6 @@ if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "
info " - $BUILDINFO" info " - $BUILDINFO"
echo echo
# lftp build dir change
LFTP_SCRIPT=$(mktemp) LFTP_SCRIPT=$(mktemp)
cat >"$LFTP_SCRIPT" <<EOF cat >"$LFTP_SCRIPT" <<EOF
cd ~avengemedia/ubuntu/$PPA_NAME/ cd ~avengemedia/ubuntu/$PPA_NAME/
@@ -148,17 +282,11 @@ EOF
exit 1 exit 1
fi fi
else else
# Use dput for other PPAs # This branch should not be reached for DMS packages
if [ ! -f "$UPLOAD_SCRIPT" ]; then # All DMS packages (dms, dms-git, dms-greeter) use lftp
error "Upload script not found: $UPLOAD_SCRIPT" error "Unknown PPA: $PPA_NAME"
error "DMS packages use lftp for upload. Supported PPAs: dms, dms-git, danklinux"
exit 1 exit 1
fi
# Auto-confirm upload (pipe 'y' to the confirmation prompt)
if ! echo "y" | "$UPLOAD_SCRIPT" "$CHANGES_FILE" "$PPA_NAME"; then
error "Upload failed!"
exit 1
fi
fi fi
echo echo
@@ -167,11 +295,17 @@ info "Monitor build progress at:"
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages" echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
echo echo
# Step 3: Cleanup (unless --keep-builds is specified)
if [ "$KEEP_BUILDS" = "false" ]; then if [ "$KEEP_BUILDS" = "false" ]; then
info "Step 3: Cleaning up build artifacts..." info "Step 3: Cleaning up build artifacts..."
# Find all build artifacts in parent directory if [ -n "${BUILD_TEMP_DIR:-}" ] && [ "$BUILD_TEMP_DIR" != "$PARENT_DIR" ]; then
if [ -d "$BUILD_TEMP_DIR" ]; then
info "Removing temp build directory: $BUILD_TEMP_DIR"
rm -rf "$BUILD_TEMP_DIR"
fi
fi
rm -f "$PARENT_DIR/.ppa_build_temp_${PACKAGE_NAME}"
ARTIFACTS=( ARTIFACTS=(
"${PACKAGE_NAME}_*.dsc" "${PACKAGE_NAME}_*.dsc"
"${PACKAGE_NAME}_*.tar.xz" "${PACKAGE_NAME}_*.tar.xz"
@@ -191,7 +325,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
done done
done done
# Clean up downloaded binaries in package directory
case "$PACKAGE_NAME" in case "$PACKAGE_NAME" in
danksearch) danksearch)
if [ -f "$PACKAGE_DIR/dsearch-amd64" ]; then if [ -f "$PACKAGE_DIR/dsearch-amd64" ]; then
@@ -204,7 +337,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
fi fi
;; ;;
dms) dms)
# Remove downloaded binaries and source
if [ -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" ]; then if [ -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" ]; then
rm -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" rm -f "$PACKAGE_DIR/dms-distropkg-amd64.gz"
REMOVED=$((REMOVED + 1)) REMOVED=$((REMOVED + 1))
@@ -1 +0,0 @@
danksearch_0.0.7ppa3_source.buildinfo utils optional
@@ -1 +0,0 @@
dgop_0.1.11ppa2_source.buildinfo utils optional
+3 -4
View File
@@ -1,6 +1,5 @@
dms-git (0.6.2+git2419.993f14a3) questing; urgency=medium dms-git (1.0.2+git2531.208266dfppa1) questing; urgency=medium
* widgets: make dank icon picker a popup * Git snapshot (commit 2531: 208266df)
* Previous updates included in build
-- Avenge Media <AvengeMedia.US@gmail.com> Mon, 09 Dec 2025 14:00:00 +0000 -- Avenge Media <AvengeMedia.US@gmail.com> Sun, 14 Dec 2025 15:20:45 +0000
+1 -1
View File
@@ -1 +1 @@
dms-git_0.6.2+git2169.f7f1bbbdppa10_source.buildinfo x11 optional dms-git_1.0.2+git2491.db2f68e3ppa4_source.buildinfo x11 optional
+3 -3
View File
@@ -1,5 +1,5 @@
dms-greeter (0.6.2ppa3) questing; urgency=medium dms-greeter (1.0.2ppa2) questing; urgency=medium
* Rebuild for packaging fixes (ppa3) * Rebuild for packaging fixes (ppa2)
-- Avenge Media <AvengeMedia.US@gmail.com> Thu, 27 Nov 2025 23:38:37 -0500 -- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:50:44 +0000
-1
View File
@@ -1 +0,0 @@
dms-greeter_0.6.2ppa3_source.buildinfo x11 optional
+3 -3
View File
@@ -1,5 +1,5 @@
dms (1.0.0ppa4) questing; urgency=medium dms (1.0.2ppa2) questing; urgency=medium
* Rebuild for packaging fixes (ppa4) * Rebuild for packaging fixes (ppa2)
-- Avenge Media <AvengeMedia.US@gmail.com> Wed, 10 Dec 2025 12:56:23 -0500 -- Avenge Media <AvengeMedia.US@gmail.com> Sat, 13 Dec 2025 06:46:52 +0000
-1
View File
@@ -1 +0,0 @@
dms_1.0.0ppa4_source.buildinfo x11 optional
+12 -2
View File
@@ -44,6 +44,10 @@
pkgs: qmlPkgs: pkgs: qmlPkgs:
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs); pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs);
mkQtPluginPath =
pkgs: qtPkgs:
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtPluginPrefix}") qtPkgs);
qmlPkgs = qmlPkgs =
pkgs: with pkgs.kdePackages; [ pkgs: with pkgs.kdePackages; [
kirigami.unwrapped kirigami.unwrapped
@@ -78,7 +82,7 @@
inherit version; inherit version;
pname = "dms-shell"; pname = "dms-shell";
src = ./core; src = ./core;
vendorHash = "sha256-yqV12LssYV0zuUPLjTzJE0e49uUER95dRH4LTcRJeGc="; vendorHash = "sha256-DINaA5LCOWoxBIewuc39Rnwj6NdZoET7Q++B11Qg5rI=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
@@ -108,7 +112,8 @@
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" --prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \ install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service $out/lib/systemd/user/dms.service
@@ -174,6 +179,10 @@
prek prek
uv # for prek uv # for prek
# Nix development tools
nixd
nil
] ]
++ devQmlPkgs; ++ devQmlPkgs;
@@ -183,6 +192,7 @@
''; '';
QML2_IMPORT_PATH = mkQmlImportPath pkgs devQmlPkgs; QML2_IMPORT_PATH = mkQmlImportPath pkgs devQmlPkgs;
QT_PLUGIN_PATH = mkQtPluginPath pkgs devQmlPkgs;
}; };
} }
); );
+6 -2
View File
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property int settingsConfigVersion: 2 readonly property int settingsConfigVersion: 3
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -352,6 +352,7 @@ Singleton {
property string customPowerActionReboot: "" property string customPowerActionReboot: ""
property string customPowerActionPowerOff: "" property string customPowerActionPowerOff: ""
property bool updaterHideWidget: false
property bool updaterUseCustomCommand: false property bool updaterUseCustomCommand: false
property string updaterCustomCommand: "" property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: "" property string updaterTerminalAdditionalParams: ""
@@ -396,7 +397,10 @@ Singleton {
visible: true, visible: true,
popupGapsAuto: true, popupGapsAuto: true,
popupGapsManual: 4, popupGapsManual: 4,
maximizeDetection: true maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace"
} }
] ]
-4
View File
@@ -203,10 +203,6 @@ Singleton {
"value": "scheme-vibrant", "value": "scheme-vibrant",
"label": "Vibrant", "label": "Vibrant",
"description": I18n.tr("Lively palette with saturated accents.") "description": I18n.tr("Lively palette with saturated accents.")
}), ({
"value": "scheme-dynamic-contrast",
"label": "Dynamic Contrast",
"description": I18n.tr("High-contrast palette for strong visual distinction.")
}), ({ }), ({
"value": "scheme-content", "value": "scheme-content",
"label": "Content", "label": "Content",
+53 -27
View File
@@ -1,5 +1,4 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
@@ -17,40 +16,67 @@ Singleton {
pciId: "", pciId: "",
mountPath: "/", mountPath: "/",
minimumWidth: true, minimumWidth: true,
showSwap: false showSwap: false,
} mediaSize: 1,
leftModel.append(dummy) showNetworkIcon: true,
centerModel.append(dummy) showBluetoothIcon: true,
rightModel.append(dummy) showAudioIcon: true,
showVpnIcon: true,
showBrightnessIcon: false,
showMicIcon: false,
showBatteryIcon: false,
showPrinterIcon: false
};
leftModel.append(dummy);
centerModel.append(dummy);
rightModel.append(dummy);
update(leftModel, left) update(leftModel, left);
update(centerModel, center) update(centerModel, center);
update(rightModel, right) update(rightModel, right);
} }
function update(model, order) { function update(model, order) {
model.clear() model.clear();
for (var i = 0; i < order.length; i++) { for (var i = 0; i < order.length; i++) {
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id var isObj = typeof order[i] !== "string";
var enabled = typeof order[i] === "string" ? true : order[i].enabled var widgetId = isObj ? order[i].id : order[i];
var size = typeof order[i] === "string" ? undefined : order[i].size
var selectedGpuIndex = typeof order[i] === "string" ? undefined : order[i].selectedGpuIndex
var pciId = typeof order[i] === "string" ? undefined : order[i].pciId
var mountPath = typeof order[i] === "string" ? undefined : order[i].mountPath
var minimumWidth = typeof order[i] === "string" ? undefined : order[i].minimumWidth
var showSwap = typeof order[i] === "string" ? undefined : order[i].showSwap
var item = { var item = {
widgetId: widgetId, widgetId: widgetId,
enabled: enabled enabled: isObj ? order[i].enabled : true
} };
if (size !== undefined) item.size = size if (isObj && order[i].size !== undefined)
if (selectedGpuIndex !== undefined) item.selectedGpuIndex = selectedGpuIndex item.size = order[i].size;
if (pciId !== undefined) item.pciId = pciId if (isObj && order[i].selectedGpuIndex !== undefined)
if (mountPath !== undefined) item.mountPath = mountPath item.selectedGpuIndex = order[i].selectedGpuIndex;
if (minimumWidth !== undefined) item.minimumWidth = minimumWidth if (isObj && order[i].pciId !== undefined)
if (showSwap !== undefined) item.showSwap = showSwap item.pciId = order[i].pciId;
if (isObj && order[i].mountPath !== undefined)
item.mountPath = order[i].mountPath;
if (isObj && order[i].minimumWidth !== undefined)
item.minimumWidth = order[i].minimumWidth;
if (isObj && order[i].showSwap !== undefined)
item.showSwap = order[i].showSwap;
if (isObj && order[i].mediaSize !== undefined)
item.mediaSize = order[i].mediaSize;
if (isObj && order[i].showNetworkIcon !== undefined)
item.showNetworkIcon = order[i].showNetworkIcon;
if (isObj && order[i].showBluetoothIcon !== undefined)
item.showBluetoothIcon = order[i].showBluetoothIcon;
if (isObj && order[i].showAudioIcon !== undefined)
item.showAudioIcon = order[i].showAudioIcon;
if (isObj && order[i].showVpnIcon !== undefined)
item.showVpnIcon = order[i].showVpnIcon;
if (isObj && order[i].showBrightnessIcon !== undefined)
item.showBrightnessIcon = order[i].showBrightnessIcon;
if (isObj && order[i].showMicIcon !== undefined)
item.showMicIcon = order[i].showMicIcon;
if (isObj && order[i].showBatteryIcon !== undefined)
item.showBatteryIcon = order[i].showBatteryIcon;
if (isObj && order[i].showPrinterIcon !== undefined)
item.showPrinterIcon = order[i].showPrinterIcon;
model.append(item) model.append(item);
} }
} }
} }
+5 -1
View File
@@ -250,6 +250,7 @@ var SPEC = {
customPowerActionReboot: { def: "" }, customPowerActionReboot: { def: "" },
customPowerActionPowerOff: { def: "" }, customPowerActionPowerOff: { def: "" },
updaterHideWidget: { def: false },
updaterUseCustomCommand: { def: false }, updaterUseCustomCommand: { def: false },
updaterCustomCommand: { def: "" }, updaterCustomCommand: { def: "" },
updaterTerminalAdditionalParams: { def: "" }, updaterTerminalAdditionalParams: { def: "" },
@@ -293,7 +294,10 @@ var SPEC = {
visible: true, visible: true,
popupGapsAuto: true, popupGapsAuto: true,
popupGapsManual: 4, popupGapsManual: 4,
maximizeDetection: true maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace"
}], onChange: "updateBarConfigs" } }], onChange: "updateBarConfigs" }
}; };
@@ -113,6 +113,12 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 2; settings.configVersion = 2;
} }
if (currentVersion < 3) {
console.info("Migrating settings from version", currentVersion, "to version 3");
console.info("Per-widget controlCenterButton config now supported via widgetData properties");
settings.configVersion = 3;
}
return settings; return settings;
} }
+50 -9
View File
@@ -49,12 +49,14 @@ Item {
readonly property alias backgroundWindow: backgroundWindow readonly property alias backgroundWindow: backgroundWindow
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useSingleWindow: root.useHyprlandFocusGrab
signal opened signal opened
signal dialogClosed signal dialogClosed
signal backgroundClicked signal backgroundClicked
property bool animationsEnabled: true property bool animationsEnabled: true
readonly property bool useBackgroundWindow: true readonly property bool useBackgroundWindow: !useSingleWindow
function open() { function open() {
ModalManager.openModal(root); ModalManager.openModal(root);
@@ -205,7 +207,7 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible enabled: root.useBackgroundWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => { onClicked: mouse => {
const clickX = mouse.x; const clickX = mouse.x;
const clickY = mouse.y; const clickY = mouse.y;
@@ -222,7 +224,7 @@ Item {
anchors.fill: parent anchors.fill: parent
color: "black" color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground visible: root.useBackgroundWindow && root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled enabled: root.animationsEnabled
@@ -271,15 +273,19 @@ Item {
anchors { anchors {
left: true left: true
top: true top: true
right: root.useSingleWindow
bottom: root.useSingleWindow
} }
WlrLayershell.margins { WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr)) left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr)) top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
right: 0
bottom: 0
} }
implicitWidth: root.alignedWidth + (shadowBuffer * 2) implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.alignedHeight + (shadowBuffer * 2) implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
@@ -292,13 +298,48 @@ Item {
} }
} }
MouseArea {
anchors.fill: parent
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
z: -2
onClicked: root.backgroundClicked()
}
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useSingleWindow && root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
Item { Item {
id: modalContainer id: modalContainer
x: shadowBuffer x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: shadowBuffer y: root.useSingleWindow ? root.alignedY : shadowBuffer
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
MouseArea {
anchors.fill: parent
enabled: root.useSingleWindow
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
readonly property bool slide: root.animationType === "slide" readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0 readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset readonly property real offsetY: slide ? -30 : root.animationOffset
+44 -77
View File
@@ -7,10 +7,11 @@ DankModal {
id: root id: root
property string outputName: "" property string outputName: ""
property var position: undefined property var changes: []
property var mode: undefined property int countdown: 10
property var vrr: undefined
property int countdown: 15 signal confirmed
signal reverted
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
@@ -23,23 +24,27 @@ DankModal {
repeat: true repeat: true
running: root.shouldBeVisible running: root.shouldBeVisible
onTriggered: { onTriggered: {
countdown--; root.countdown--
if (countdown <= 0) { if (root.countdown <= 0) {
revert(); root.reverted()
root.close()
} }
} }
} }
onOpened: { onOpened: {
countdown = 15; countdown = 10
countdownTimer.start(); countdownTimer.start()
} }
onClosed: { onDialogClosed: {
countdownTimer.stop(); countdownTimer.stop()
} }
onBackgroundClicked: revert onBackgroundClicked: {
root.reverted()
root.close()
}
content: Component { content: Component {
FocusScope { FocusScope {
@@ -50,13 +55,15 @@ DankModal {
implicitHeight: mainColumn.implicitHeight implicitHeight: mainColumn.implicitHeight
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
revert(); root.reverted()
event.accepted = true; root.close()
event.accepted = true
} }
Keys.onReturnPressed: event => { Keys.onReturnPressed: event => {
confirm(); root.confirmed()
event.accepted = true; root.close()
event.accepted = true
} }
Column { Column {
@@ -69,10 +76,6 @@ DankModal {
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
spacing: Theme.spacingM spacing: Theme.spacingM
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText { StyledText {
text: I18n.tr("Confirm Display Changes") text: I18n.tr("Confirm Display Changes")
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
@@ -80,70 +83,35 @@ DankModal {
font.weight: Font.Medium font.weight: Font.Medium
} }
StyledText {
text: I18n.tr("Display settings for ") + outputName
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
}
}
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 80 height: 70
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest color: Theme.surfaceContainerHighest
Column { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 4 text: root.countdown + "s"
StyledText {
text: I18n.tr("Reverting in:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: countdown + "s"
font.pixelSize: Theme.fontSizeXLarge * 1.5 font.pixelSize: Theme.fontSizeXLarge * 1.5
color: Theme.primary color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
} }
} }
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: root.changes.length > 0
Repeater {
model: root.changes
StyledText { StyledText {
text: I18n.tr("Changes:") required property var modelData
text: modelData
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceVariantText
font.weight: Font.Medium
} }
StyledText {
visible: position !== undefined && position !== null
text: I18n.tr("Position: ") + (position ? position.x + ", " + position.y : "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
visible: mode !== undefined && mode !== null && mode !== ""
text: I18n.tr("Mode: ") + (mode || "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
visible: vrr !== undefined && vrr !== null
text: I18n.tr("VRR: ") + (vrr ? I18n.tr("Enabled") : I18n.tr("Disabled"))
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
} }
} }
@@ -180,7 +148,10 @@ DankModal {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: revert onClicked: {
root.reverted()
root.close()
}
} }
} }
@@ -206,7 +177,10 @@ DankModal {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: confirm onClicked: {
root.confirmed()
root.close()
}
} }
Behavior on color { Behavior on color {
@@ -228,18 +202,11 @@ DankModal {
iconName: "close" iconName: "close"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: revert onClicked: {
root.reverted()
root.close()
} }
} }
} }
function confirm() {
displaysTab.confirmChanges();
close();
}
function revert() {
displaysTab.revertChanges();
close();
} }
} }
+6
View File
@@ -98,6 +98,12 @@ DankModal {
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS"; return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
} }
function toggleDoNotDisturb(): string {
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
return "NOTIFICATION_MODAL_TOGGLE_DND_SUCCESS";
}
target: "notifications" target: "notifications"
} }
+35 -3
View File
@@ -125,13 +125,45 @@ FocusScope {
} }
Loader { Loader {
id: displaysLoader id: displayConfigLoader
anchors.fill: parent anchors.fill: parent
active: root.currentIndex === 6 active: root.currentIndex === 24
visible: active visible: active
focus: active focus: active
sourceComponent: DisplaysTab {} sourceComponent: DisplayConfigTab {}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus());
}
}
}
Loader {
id: gammaControlLoader
anchors.fill: parent
active: root.currentIndex === 25
visible: active
focus: active
sourceComponent: GammaControlTab {}
onActiveChanged: {
if (active && item) {
Qt.callLater(() => item.forceActiveFocus());
}
}
}
Loader {
id: displayWidgetsLoader
anchors.fill: parent
active: root.currentIndex === 26
visible: active
focus: active
sourceComponent: DisplayWidgetsTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) { if (active && item) {
+1 -1
View File
@@ -58,7 +58,7 @@ FloatingWindow {
objectName: "settingsModal" objectName: "settingsModal"
title: I18n.tr("Settings", "settings window title") title: I18n.tr("Settings", "settings window title")
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 800 implicitWidth: 900
implicitHeight: screen ? Math.min(940, screen.height - 100) : 940 implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
color: Theme.surfaceContainer color: Theme.surfaceContainer
visible: false visible: false
+26 -6
View File
@@ -144,6 +144,32 @@ Rectangle {
"tabIndex": 2, "tabIndex": 2,
"shortcutsOnly": true "shortcutsOnly": true
}, },
{
"id": "displays",
"text": I18n.tr("Displays"),
"icon": "monitor",
"collapsedByDefault": true,
"children": [
{
"id": "display_config",
"text": I18n.tr("Configuration") + " (Beta)",
"icon": "display_settings",
"tabIndex": 24
},
{
"id": "display_gamma",
"text": I18n.tr("Gamma Control"),
"icon": "brightness_6",
"tabIndex": 25
},
{
"id": "display_widgets",
"text": I18n.tr("Widgets", "settings_displays"),
"icon": "widgets",
"tabIndex": 26
}
]
},
{ {
"id": "network", "id": "network",
"text": I18n.tr("Network"), "text": I18n.tr("Network"),
@@ -157,12 +183,6 @@ Rectangle {
"icon": "computer", "icon": "computer",
"collapsedByDefault": true, "collapsedByDefault": true,
"children": [ "children": [
{
"id": "displays",
"text": I18n.tr("Displays"),
"icon": "monitor",
"tabIndex": 6
},
{ {
"id": "printers", "id": "printers",
"text": I18n.tr("Printers"), "text": I18n.tr("Printers"),
@@ -27,7 +27,7 @@ PluginComponent {
ccDetailContent: Component { ccDetailContent: Component {
VpnDetailContent { VpnDetailContent {
listHeight: 180 listHeight: 260
} }
} }
} }
@@ -18,14 +18,17 @@ Item {
function getDetailHeight(section) { function getDetailHeight(section) {
const maxAvailable = parent ? parent.height - Theme.spacingS : 9999; const maxAvailable = parent ? parent.height - Theme.spacingS : 9999;
if (section === "wifi") switch (true) {
case section === "wifi":
case section === "bluetooth":
case section === "builtin_vpn":
return Math.min(350, maxAvailable); return Math.min(350, maxAvailable);
if (section === "bluetooth") case section.startsWith("brightnessSlider_"):
return Math.min(350, maxAvailable);
if (section.startsWith("brightnessSlider_"))
return Math.min(400, maxAvailable); return Math.min(400, maxAvailable);
default:
return Math.min(250, maxAvailable); return Math.min(250, maxAvailable);
} }
}
Loader { Loader {
id: pluginDetailLoader id: pluginDetailLoader
+60 -10
View File
@@ -594,7 +594,8 @@ PanelWindow {
propagateComposedEvents: true propagateComposedEvents: true
z: -1 z: -1
property real scrollAccumulator: 0 property real scrollAccumulatorY: 0
property real scrollAccumulatorX: 0
property real touchpadThreshold: 500 property real touchpadThreshold: 500
property bool actionInProgress: false property bool actionInProgress: false
@@ -604,7 +605,30 @@ PanelWindow {
onTriggered: parent.actionInProgress = false onTriggered: parent.actionInProgress = false
} }
function handleScrollAction(behavior, direction) {
switch (behavior) {
case "workspace":
topBarContent.switchWorkspace(direction);
return true;
case "column":
if (!CompositorService.isNiri)
return false;
if (direction > 0)
NiriService.moveColumnRight();
else
NiriService.moveColumnLeft();
return true;
default:
return false;
}
}
onWheel: wheel => { onWheel: wheel => {
if (!(barConfig?.scrollEnabled ?? true)) {
wheel.accepted = false;
return;
}
if (actionInProgress) { if (actionInProgress) {
wheel.accepted = false; wheel.accepted = false;
return; return;
@@ -612,9 +636,34 @@ PanelWindow {
const deltaY = wheel.angleDelta.y; const deltaY = wheel.angleDelta.y;
const deltaX = wheel.angleDelta.x; const deltaX = wheel.angleDelta.x;
const xBehavior = barConfig?.scrollXBehavior ?? "column";
const yBehavior = barConfig?.scrollYBehavior ?? "workspace";
if (CompositorService.isNiri && Math.abs(deltaX) > Math.abs(deltaY)) { if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) {
topBarContent.switchApp(deltaX); const isMouseWheel = Math.abs(deltaX) >= 120 && (Math.abs(deltaX) % 120) === 0;
const direction = deltaX < 0 ? 1 : -1;
if (isMouseWheel) {
if (handleScrollAction(xBehavior, direction)) {
actionInProgress = true;
cooldownTimer.restart();
}
} else {
scrollAccumulatorX += deltaX;
if (Math.abs(scrollAccumulatorX) >= touchpadThreshold) {
const touchDirection = scrollAccumulatorX < 0 ? 1 : -1;
if (handleScrollAction(xBehavior, touchDirection)) {
actionInProgress = true;
cooldownTimer.restart();
}
scrollAccumulatorX = 0;
}
}
wheel.accepted = false;
return;
}
if (yBehavior === "none") {
wheel.accepted = false; wheel.accepted = false;
return; return;
} }
@@ -623,19 +672,20 @@ PanelWindow {
const direction = deltaY < 0 ? 1 : -1; const direction = deltaY < 0 ? 1 : -1;
if (isMouseWheel) { if (isMouseWheel) {
topBarContent.switchWorkspace(direction); if (handleScrollAction(yBehavior, direction)) {
actionInProgress = true; actionInProgress = true;
cooldownTimer.restart(); cooldownTimer.restart();
}
} else { } else {
scrollAccumulator += deltaY; scrollAccumulatorY += deltaY;
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
if (Math.abs(scrollAccumulator) >= touchpadThreshold) { const touchDirection = scrollAccumulatorY < 0 ? 1 : -1;
const touchDirection = scrollAccumulator < 0 ? 1 : -1; if (handleScrollAction(yBehavior, touchDirection)) {
topBarContent.switchWorkspace(touchDirection);
scrollAccumulator = 0;
actionInProgress = true; actionInProgress = true;
cooldownTimer.restart(); cooldownTimer.restart();
} }
scrollAccumulatorY = 0;
}
} }
wheel.accepted = false; wheel.accepted = false;
@@ -552,7 +552,13 @@ DankPopout {
} }
} }
Item {
width: parent.width
height: profileButtonGroup.height * profileButtonGroup.scale
DankButtonGroup { DankButtonGroup {
id: profileButtonGroup
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: { property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined") if (typeof PowerProfiles === "undefined")
@@ -560,16 +566,19 @@ DankPopout {
return profileModel.findIndex(profile => root.isActiveProfile(profile)); return profileModel.findIndex(profile => root.isActiveProfile(profile));
} }
scale: Math.min(1, parent.width / implicitWidth)
transformOrigin: Item.Center
anchors.horizontalCenter: parent.horizontalCenter
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile)) model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
currentIndex: currentProfileIndex currentIndex: currentProfileIndex
selectionMode: "single" selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) if (!selected)
return; return;
root.setProfile(profileModel[index]); root.setProfile(profileModel[index]);
} }
} }
}
StyledRect { StyledRect {
width: parent.width width: parent.width
@@ -15,6 +15,7 @@ DankPopout {
triggerY = y; triggerY = y;
triggerWidth = width; triggerWidth = width;
triggerSection = section; triggerSection = section;
triggerScreen = screen;
root.screen = screen; root.screen = screen;
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4); storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
@@ -102,6 +103,8 @@ DankPopout {
screen: triggerScreen screen: triggerScreen
shouldBeVisible: false shouldBeVisible: false
onBackgroundClicked: close()
content: Component { content: Component {
Rectangle { Rectangle {
id: layoutContent id: layoutContent
@@ -13,14 +13,14 @@ BasePill {
property var widgetData: null property var widgetData: null
property string screenName: "" property string screenName: ""
property string screenModel: "" property string screenModel: ""
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon property bool showNetworkIcon: widgetData?.showNetworkIcon !== undefined ? widgetData.showNetworkIcon : SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon property bool showBluetoothIcon: widgetData?.showBluetoothIcon !== undefined ? widgetData.showBluetoothIcon : SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon property bool showAudioIcon: widgetData?.showAudioIcon !== undefined ? widgetData.showAudioIcon : SettingsData.controlCenterShowAudioIcon
property bool showVpnIcon: SettingsData.controlCenterShowVpnIcon property bool showVpnIcon: widgetData?.showVpnIcon !== undefined ? widgetData.showVpnIcon : SettingsData.controlCenterShowVpnIcon
property bool showBrightnessIcon: SettingsData.controlCenterShowBrightnessIcon property bool showBrightnessIcon: widgetData?.showBrightnessIcon !== undefined ? widgetData.showBrightnessIcon : SettingsData.controlCenterShowBrightnessIcon
property bool showMicIcon: SettingsData.controlCenterShowMicIcon property bool showMicIcon: widgetData?.showMicIcon !== undefined ? widgetData.showMicIcon : SettingsData.controlCenterShowMicIcon
property bool showBatteryIcon: SettingsData.controlCenterShowBatteryIcon property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: SettingsData.controlCenterShowPrinterIcon property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon
Loader { Loader {
active: root.showPrinterIcon active: root.showPrinterIcon
@@ -358,7 +358,7 @@ Item {
IconImage { IconImage {
id: iconImg id: iconImg
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness) width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness) height: Theme.barIconSize(root.barThickness)
@@ -385,7 +385,7 @@ Item {
DankIcon { DankIcon {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
name: "sports_esports" name: "sports_esports"
@@ -607,7 +607,7 @@ Item {
IconImage { IconImage {
id: iconImg id: iconImg
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness) width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness) height: Theme.barIconSize(root.barThickness)
@@ -634,7 +634,7 @@ Item {
DankIcon { DankIcon {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS anchors.leftMargin: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? Math.round((parent.width - Theme.barIconSize(root.barThickness)) / 2) : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
name: "sports_esports" name: "sports_esports"
@@ -11,6 +11,9 @@ BasePill {
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0 readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
readonly property bool isChecking: SystemUpdateService.isChecking readonly property bool isChecking: SystemUpdateService.isChecking
readonly property real horizontalPadding: (barConfig?.noBackground ?? false) ? 2 : Theme.spacingS
width : (SettingsData.updaterHideWidget && !hasUpdates) ? 0 : (18 + horizontalPadding * 2)
Ref { Ref {
service: SystemUpdateService service: SystemUpdateService
} }
@@ -649,6 +649,16 @@ Item {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton acceptedButtons: Qt.RightButton
property real scrollAccumulator: 0
property real touchpadThreshold: 500
property bool scrollInProgress: false
Timer {
id: scrollCooldown
interval: 100
onTriggered: parent.scrollInProgress = false
}
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
@@ -658,6 +668,30 @@ Item {
} }
} }
} }
onWheel: wheel => {
if (scrollInProgress)
return;
const delta = wheel.angleDelta.y;
const isMouseWheel = Math.abs(delta) >= 120 && (Math.abs(delta) % 120) === 0;
const direction = delta < 0 ? 1 : -1;
if (isMouseWheel) {
root.switchWorkspace(direction);
scrollInProgress = true;
scrollCooldown.restart();
} else {
scrollAccumulator += delta;
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
const touchDirection = scrollAccumulator < 0 ? 1 : -1;
root.switchWorkspace(touchDirection);
scrollInProgress = true;
scrollCooldown.restart();
scrollAccumulator = 0;
}
}
}
} }
Flow { Flow {
@@ -808,7 +842,12 @@ Item {
wsData = modelData; wsData = modelData;
} }
delegateRoot.loadedWorkspaceData = wsData; delegateRoot.loadedWorkspaceData = wsData;
if (CompositorService.isNiri) {
const workspaceId = wsData?.id;
delegateRoot.loadedIsUrgent = workspaceId ? NiriService.windows.some(w => w.workspace_id === workspaceId && w.is_urgent) : false;
} else {
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false; delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
}
var icData = null; var icData = null;
if (wsData?.name) { if (wsData?.name) {
@@ -844,8 +883,8 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
border.width: isUrgent && !isActive ? 2 : 0 border.width: isUrgent ? 2 : 0
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0) border.color: isUrgent ? Theme.error : Theme.withAlpha(Theme.error, 0)
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
+214 -240
View File
@@ -1,13 +1,11 @@
import QtCore import QtCore
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Greetd import Quickshell.Services.Greetd
import Quickshell.Services.Pam
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -22,74 +20,67 @@ Item {
property string hyprlandCurrentLayout: "" property string hyprlandCurrentLayout: ""
property string hyprlandKeyboard: "" property string hyprlandKeyboard: ""
property int hyprlandLayoutCount: 0 property int hyprlandLayoutCount: 0
property bool isPrimaryScreen: { property bool isPrimaryScreen: !Quickshell.screens?.length || screenName === Quickshell.screens[0]?.name
if (!Qt.application.screens || Qt.application.screens.length === 0)
return true
if (!screenName || screenName === "")
return true
return screenName === Qt.application.screens[0].name
}
signal launchRequested signal launchRequested
function pickRandomFact() { function pickRandomFact() {
randomFact = Facts.getRandomFact() randomFact = Facts.getRandomFact();
} }
property bool weatherInitialized: false property bool weatherInitialized: false
function initWeatherService() { function initWeatherService() {
if (weatherInitialized) if (weatherInitialized)
return return;
if (!GreetdSettings.settingsLoaded) if (!GreetdSettings.settingsLoaded)
return return;
if (!GreetdSettings.weatherEnabled) if (!GreetdSettings.weatherEnabled)
return return;
weatherInitialized = true;
weatherInitialized = true WeatherService.addRef();
WeatherService.addRef() WeatherService.forceRefresh();
WeatherService.forceRefresh()
} }
Connections { Connections {
target: GreetdSettings target: GreetdSettings
function onSettingsLoadedChanged() { function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded) if (GreetdSettings.settingsLoaded)
initWeatherService() initWeatherService();
} }
} }
Component.onCompleted: { Component.onCompleted: {
pickRandomFact() pickRandomFact();
initWeatherService() initWeatherService();
if (isPrimaryScreen) { if (isPrimaryScreen) {
sessionListProc.running = true sessionListProc.running = true;
applyLastSuccessfulUser() applyLastSuccessfulUser();
} }
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
updateHyprlandLayout() updateHyprlandLayout();
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
const lastUser = GreetdMemory.lastSuccessfulUser const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser) PortalService.getGreeterUserProfileImage(lastUser);
} }
} }
Component.onDestruction: { Component.onDestruction: {
if (weatherInitialized) if (weatherInitialized)
WeatherService.removeRef() WeatherService.removeRef();
} }
function updateHyprlandLayout() { function updateHyprlandLayout() {
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true hyprlandLayoutProcess.running = true;
} }
} }
@@ -100,27 +91,27 @@ Item {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
try { try {
const data = JSON.parse(text) const data = JSON.parse(text);
const mainKeyboard = data.keyboards.find(kb => kb.main === true) const mainKeyboard = data.keyboards.find(kb => kb.main === true);
hyprlandKeyboard = mainKeyboard.name hyprlandKeyboard = mainKeyboard.name;
if (mainKeyboard && mainKeyboard.active_keymap) { if (mainKeyboard && mainKeyboard.active_keymap) {
const parts = mainKeyboard.active_keymap.split(" ") const parts = mainKeyboard.active_keymap.split(" ");
if (parts.length > 0) { if (parts.length > 0) {
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase() hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase();
} else { } else {
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase() hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase();
} }
} else { } else {
hyprlandCurrentLayout = "" hyprlandCurrentLayout = "";
} }
if (mainKeyboard && mainKeyboard.layout_names) { if (mainKeyboard && mainKeyboard.layout_names) {
hyprlandLayoutCount = mainKeyboard.layout_names.length hyprlandLayoutCount = mainKeyboard.layout_names.length;
} else { } else {
hyprlandLayoutCount = 0 hyprlandLayoutCount = 0;
} }
} catch (e) { } catch (e) {
hyprlandCurrentLayout = "" hyprlandCurrentLayout = "";
hyprlandLayoutCount = 0 hyprlandLayoutCount = 0;
} }
} }
} }
@@ -132,7 +123,7 @@ Item {
function onRawEvent(event) { function onRawEvent(event) {
if (event.name === "activelayout") if (event.name === "activelayout")
updateHyprlandLayout() updateHyprlandLayout();
} }
} }
@@ -140,7 +131,7 @@ Item {
target: GreetdMemory target: GreetdMemory
enabled: isPrimaryScreen enabled: isPrimaryScreen
function onLastSuccessfulUserChanged() { function onLastSuccessfulUserChanged() {
applyLastSuccessfulUser() applyLastSuccessfulUser();
} }
} }
@@ -148,7 +139,7 @@ Item {
target: GreeterState target: GreeterState
function onUsernameChanged() { function onUsernameChanged() {
if (GreeterState.username) { if (GreeterState.username) {
PortalService.getGreeterUserProfileImage(GreeterState.username) PortalService.getGreeterUserProfileImage(GreeterState.username);
} }
} }
} }
@@ -157,10 +148,10 @@ Item {
anchors.fill: parent anchors.fill: parent
screenName: root.screenName screenName: root.screenName
visible: { visible: {
var _ = SessionData.perMonitorWallpaper var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName) var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#")) return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#"));
} }
} }
@@ -169,10 +160,10 @@ Item {
anchors.fill: parent anchors.fill: parent
source: { source: {
var _ = SessionData.perMonitorWallpaper var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName) var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "" return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "";
} }
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode) fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
smooth: true smooth: true
@@ -213,10 +204,12 @@ Item {
color: "transparent" color: "transparent"
Item { Item {
anchors.centerIn: parent id: clockContainer
anchors.verticalCenterOffset: -100 anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.verticalCenter
anchors.bottomMargin: 60
width: parent.width width: parent.width
height: 140 height: clockText.implicitHeight
Row { Row {
id: clockText id: clockText
@@ -225,10 +218,8 @@ Item {
spacing: 0 spacing: 0
property string fullTimeStr: { property string fullTimeStr: {
const format = GreetdSettings.use24HourClock const format = GreetdSettings.use24HourClock ? (GreetdSettings.showSeconds ? "HH:mm:ss" : "HH:mm") : (GreetdSettings.showSeconds ? "h:mm:ss AP" : "h:mm AP");
? (GreetdSettings.showSeconds ? "HH:mm:ss" : "HH:mm") return systemClock.date.toLocaleTimeString(Qt.locale(), format);
: (GreetdSettings.showSeconds ? "h:mm:ss AP" : "h:mm AP")
return systemClock.date.toLocaleTimeString(Qt.locale(), format)
} }
property var timeParts: fullTimeStr.split(':') property var timeParts: fullTimeStr.split(':')
property string hours: timeParts[0] || "" property string hours: timeParts[0] || ""
@@ -236,8 +227,8 @@ Item {
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : "" property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '') property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
property string ampm: { property string ampm: {
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i) const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
return match ? match[0].trim() : "" return match ? match[0].trim() : "";
} }
property bool hasSeconds: timeParts.length > 2 property bool hasSeconds: timeParts.length > 2
@@ -332,13 +323,15 @@ Item {
} }
StyledText { StyledText {
anchors.centerIn: parent id: dateText
anchors.verticalCenterOffset: -10 anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockContainer.bottom
anchors.topMargin: 4
text: { text: {
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) { if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat) return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat);
} }
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat) return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
} }
font.pixelSize: Theme.fontSizeXLarge font.pixelSize: Theme.fontSizeXLarge
color: "white" color: "white"
@@ -346,8 +339,9 @@ Item {
} }
Item { Item {
anchors.centerIn: parent anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenterOffset: 80 anchors.top: dateText.bottom
anchors.topMargin: Theme.spacingL
width: 380 width: 380
height: 140 height: 140
@@ -364,14 +358,14 @@ Item {
Layout.preferredHeight: 60 Layout.preferredHeight: 60
imageSource: { imageSource: {
if (PortalService.profileImage === "") { if (PortalService.profileImage === "") {
return "" return "";
} }
if (PortalService.profileImage.startsWith("/")) { if (PortalService.profileImage.startsWith("/")) {
return "file://" + PortalService.profileImage return "file://" + PortalService.profileImage;
} }
return PortalService.profileImage return PortalService.profileImage;
} }
fallbackIcon: "person" fallbackIcon: "person"
} }
@@ -405,57 +399,58 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2 anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.rightMargin: { anchors.rightMargin: {
let margin = Theme.spacingM let margin = Theme.spacingM;
if (GreeterState.showPasswordInput && revealButton.visible) { if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width margin += revealButton.width;
} }
if (virtualKeyboardButton.visible) { if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width margin += virtualKeyboardButton.width;
} }
if (enterButton.visible) { if (enterButton.visible) {
margin += enterButton.width + 2 margin += enterButton.width + 2;
} }
return margin return margin;
} }
opacity: 0 opacity: 0
focus: true focus: true
echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
onTextChanged: { onTextChanged: {
if (syncingFromState) return if (syncingFromState)
return;
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text GreeterState.passwordBuffer = text;
} else { } else {
GreeterState.usernameInput = text GreeterState.usernameInput = text;
} }
} }
onAccepted: { onAccepted: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
if (Greetd.state === GreetdState.Inactive && GreeterState.username) { if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username) Greetd.createSession(GreeterState.username);
} }
} else { } else {
if (text.trim()) { if (text.trim()) {
GreeterState.username = text.trim() GreeterState.username = text.trim();
GreeterState.showPasswordInput = true GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username) PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "" GreeterState.passwordBuffer = "";
syncingFromState = true syncingFromState = true;
text = "" text = "";
syncingFromState = false syncingFromState = false;
} }
} }
} }
Component.onCompleted: { Component.onCompleted: {
syncingFromState = true syncingFromState = true;
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput;
syncingFromState = false syncingFromState = false;
if (isPrimaryScreen && !powerMenu.isVisible) if (isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus() forceActiveFocus();
} }
onVisibleChanged: { onVisibleChanged: {
if (visible && isPrimaryScreen && !powerMenu.isVisible) if (visible && isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus() forceActiveFocus();
} }
} }
@@ -475,15 +470,15 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (GreeterState.unlocking) { if (GreeterState.unlocking) {
return "Logging in..." return "Logging in...";
} }
if (Greetd.state !== GreetdState.Inactive) { if (Greetd.state !== GreetdState.Inactive) {
return "Authenticating..." return "Authenticating...";
} }
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
return "Password..." return "Password...";
} }
return "Username..." return "Username...";
} }
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline) color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -513,11 +508,11 @@ Item {
text: { text: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
if (parent.showPassword) { if (parent.showPassword) {
return GreeterState.passwordBuffer return GreeterState.passwordBuffer;
} }
return "•".repeat(GreeterState.passwordBuffer.length) return "•".repeat(GreeterState.passwordBuffer.length);
} }
return GreeterState.usernameInput return GreeterState.usernameInput;
} }
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium
@@ -558,9 +553,9 @@ Item {
enabled: visible enabled: visible
onClicked: { onClicked: {
if (keyboard_controller.isKeyboardActive) { if (keyboard_controller.isKeyboardActive) {
keyboard_controller.hide() keyboard_controller.hide();
} else { } else {
keyboard_controller.show() keyboard_controller.show();
} }
} }
} }
@@ -578,15 +573,15 @@ Item {
onClicked: { onClicked: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
if (GreeterState.username) { if (GreeterState.username) {
Greetd.createSession(GreeterState.username) Greetd.createSession(GreeterState.username);
} }
} else { } else {
if (inputField.text.trim()) { if (inputField.text.trim()) {
GreeterState.username = inputField.text.trim() GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username) PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "" GreeterState.passwordBuffer = "";
inputField.text = "" inputField.text = "";
} }
} }
} }
@@ -615,10 +610,10 @@ Item {
Layout.bottomMargin: -Theme.spacingS Layout.bottomMargin: -Theme.spacingS
text: { text: {
if (GreeterState.pamState === "error") if (GreeterState.pamState === "error")
return "Authentication error - try again" return "Authentication error - try again";
if (GreeterState.pamState === "fail") if (GreeterState.pamState === "fail")
return "Incorrect password" return "Incorrect password";
return "" return "";
} }
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -675,9 +670,9 @@ Item {
cornerRadius: parent.radius cornerRadius: parent.radius
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: { onClicked: {
GreeterState.reset() GreeterState.reset();
inputField.text = "" inputField.text = "";
PortalService.profileImage = "" PortalService.profileImage = "";
} }
} }
} }
@@ -696,11 +691,11 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: { visible: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
return NiriService.keyboardLayoutNames.length > 1 return NiriService.keyboardLayoutNames.length > 1;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
return hyprlandLayoutCount > 1 return hyprlandLayoutCount > 1;
} }
return false return false;
} }
Row { Row {
@@ -726,17 +721,18 @@ Item {
StyledText { StyledText {
text: { text: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
const layout = NiriService.getCurrentKeyboardLayoutName() const layout = NiriService.getCurrentKeyboardLayoutName();
if (!layout) return "" if (!layout)
const parts = layout.split(" ") return "";
const parts = layout.split(" ");
if (parts.length > 0) { if (parts.length > 0) {
return parts[0].substring(0, 2).toUpperCase() return parts[0].substring(0, 2).toUpperCase();
} }
return layout.substring(0, 2).toUpperCase() return layout.substring(0, 2).toUpperCase();
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
return hyprlandCurrentLayout return hyprlandCurrentLayout;
} }
return "" return "";
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Light font.weight: Font.Light
@@ -753,15 +749,10 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout() NiriService.cycleKeyboardLayout();
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
Quickshell.execDetached([ Quickshell.execDetached(["hyprctl", "switchxkblayout", hyprlandKeyboard, "next"]);
"hyprctl", updateHyprlandLayout();
"switchxkblayout",
hyprlandKeyboard,
"next"
])
updateHyprlandLayout()
} }
} }
} }
@@ -773,9 +764,8 @@ Item {
color: Qt.rgba(255, 255, 255, 0.2) color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: { visible: {
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || (CompositorService.isHyprland && hyprlandLayoutCount > 1);
(CompositorService.isHyprland && hyprlandLayoutCount > 1) return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available;
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available
} }
} }
@@ -832,15 +822,15 @@ Item {
DankIcon { DankIcon {
name: { name: {
if (!AudioService.sink?.audio) { if (!AudioService.sink?.audio) {
return "volume_up" return "volume_up";
} }
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) { if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off" return "volume_off";
} }
if (AudioService.sink.audio.volume * 100 < 33) { if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down" return "volume_down";
} }
return "volume_up" return "volume_up";
} }
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white" color: (AudioService.sink && AudioService.sink.audio && (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)) ? Qt.rgba(255, 255, 255, 0.5) : "white"
@@ -866,95 +856,95 @@ Item {
name: { name: {
if (BatteryService.isCharging) { if (BatteryService.isCharging) {
if (BatteryService.batteryLevel >= 90) { if (BatteryService.batteryLevel >= 90) {
return "battery_charging_full" return "battery_charging_full";
} }
if (BatteryService.batteryLevel >= 80) { if (BatteryService.batteryLevel >= 80) {
return "battery_charging_90" return "battery_charging_90";
} }
if (BatteryService.batteryLevel >= 60) { if (BatteryService.batteryLevel >= 60) {
return "battery_charging_80" return "battery_charging_80";
} }
if (BatteryService.batteryLevel >= 50) { if (BatteryService.batteryLevel >= 50) {
return "battery_charging_60" return "battery_charging_60";
} }
if (BatteryService.batteryLevel >= 30) { if (BatteryService.batteryLevel >= 30) {
return "battery_charging_50" return "battery_charging_50";
} }
if (BatteryService.batteryLevel >= 20) { if (BatteryService.batteryLevel >= 20) {
return "battery_charging_30" return "battery_charging_30";
} }
return "battery_charging_20" return "battery_charging_20";
} }
if (BatteryService.isPluggedIn) { if (BatteryService.isPluggedIn) {
if (BatteryService.batteryLevel >= 90) { if (BatteryService.batteryLevel >= 90) {
return "battery_charging_full" return "battery_charging_full";
} }
if (BatteryService.batteryLevel >= 80) { if (BatteryService.batteryLevel >= 80) {
return "battery_charging_90" return "battery_charging_90";
} }
if (BatteryService.batteryLevel >= 60) { if (BatteryService.batteryLevel >= 60) {
return "battery_charging_80" return "battery_charging_80";
} }
if (BatteryService.batteryLevel >= 50) { if (BatteryService.batteryLevel >= 50) {
return "battery_charging_60" return "battery_charging_60";
} }
if (BatteryService.batteryLevel >= 30) { if (BatteryService.batteryLevel >= 30) {
return "battery_charging_50" return "battery_charging_50";
} }
if (BatteryService.batteryLevel >= 20) { if (BatteryService.batteryLevel >= 20) {
return "battery_charging_30" return "battery_charging_30";
} }
return "battery_charging_20" return "battery_charging_20";
} }
if (BatteryService.batteryLevel >= 95) { if (BatteryService.batteryLevel >= 95) {
return "battery_full" return "battery_full";
} }
if (BatteryService.batteryLevel >= 85) { if (BatteryService.batteryLevel >= 85) {
return "battery_6_bar" return "battery_6_bar";
} }
if (BatteryService.batteryLevel >= 70) { if (BatteryService.batteryLevel >= 70) {
return "battery_5_bar" return "battery_5_bar";
} }
if (BatteryService.batteryLevel >= 55) { if (BatteryService.batteryLevel >= 55) {
return "battery_4_bar" return "battery_4_bar";
} }
if (BatteryService.batteryLevel >= 40) { if (BatteryService.batteryLevel >= 40) {
return "battery_3_bar" return "battery_3_bar";
} }
if (BatteryService.batteryLevel >= 25) { if (BatteryService.batteryLevel >= 25) {
return "battery_2_bar" return "battery_2_bar";
} }
return "battery_1_bar" return "battery_1_bar";
} }
size: Theme.iconSize size: Theme.iconSize
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) { if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error return Theme.error;
} }
if (BatteryService.isCharging || BatteryService.isPluggedIn) { if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary return Theme.primary;
} }
return "white" return "white";
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -1007,14 +997,14 @@ Item {
} }
property real longestSessionWidth: { property real longestSessionWidth: {
let maxWidth = 0 let maxWidth = 0;
for (var i = 0; i < sessionMetricsRepeater.count; i++) { for (var i = 0; i < sessionMetricsRepeater.count; i++) {
const item = sessionMetricsRepeater.itemAt(i) const item = sessionMetricsRepeater.itemAt(i);
if (item && item.width > maxWidth) { if (item && item.width > maxWidth) {
maxWidth = item.width maxWidth = item.width;
} }
} }
return maxWidth return maxWidth;
} }
Repeater { Repeater {
@@ -1038,52 +1028,42 @@ Item {
openUpwards: true openUpwards: true
alignPopupRight: true alignPopupRight: true
onValueChanged: value => { onValueChanged: value => {
const idx = GreeterState.sessionList.indexOf(value) const idx = GreeterState.sessionList.indexOf(value);
if (idx >= 0) { if (idx >= 0) {
GreeterState.currentSessionIndex = idx GreeterState.currentSessionIndex = idx;
GreeterState.selectedSession = GreeterState.sessionExecs[idx] GreeterState.selectedSession = GreeterState.sessionExecs[idx];
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[idx]) GreetdMemory.setLastSessionId(GreeterState.sessionPaths[idx]);
} }
} }
} }
} }
} }
FileView {
id: pamConfigWatcher
path: "/etc/pam.d/dankshell"
printErrors: false
}
property int sessionCount: 0
property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || "" property string currentSessionName: GreeterState.sessionList[GreeterState.currentSessionIndex] || ""
property int pendingParsers: 0 property int pendingParsers: 0
function finalizeSessionSelection() { function finalizeSessionSelection() {
if (GreeterState.sessionList.length === 0) { if (GreeterState.sessionList.length === 0) {
return return;
} }
root.sessionCount = GreeterState.sessionList.length const savedSession = GreetdMemory.lastSessionId;
let foundSaved = false;
const savedSession = GreetdMemory.lastSessionId
let foundSaved = false
if (savedSession) { if (savedSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) { for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) { if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i GreeterState.currentSessionIndex = i;
foundSaved = true foundSaved = true;
break break;
} }
} }
} }
if (!foundSaved) { if (!foundSaved) {
GreeterState.currentSessionIndex = 0 GreeterState.currentSessionIndex = 0;
} }
GreeterState.selectedSession = GreeterState.sessionExecs[GreeterState.currentSessionIndex] || GreeterState.sessionExecs[0] || "" GreeterState.selectedSession = GreeterState.sessionExecs[GreeterState.currentSessionIndex] || GreeterState.sessionExecs[0] || "";
} }
Process { Process {
@@ -1091,39 +1071,34 @@ Item {
property string homeDir: Quickshell.env("HOME") || "" property string homeDir: Quickshell.env("HOME") || ""
property string xdgDirs: xdgDataDirs || "" property string xdgDirs: xdgDataDirs || ""
command: { command: {
var paths = [ var paths = ["/usr/share/wayland-sessions", "/usr/share/xsessions", "/usr/local/share/wayland-sessions", "/usr/local/share/xsessions"];
"/usr/share/wayland-sessions",
"/usr/share/xsessions",
"/usr/local/share/wayland-sessions",
"/usr/local/share/xsessions"
]
if (homeDir) { if (homeDir) {
paths.push(homeDir + "/.local/share/wayland-sessions") paths.push(homeDir + "/.local/share/wayland-sessions");
paths.push(homeDir + "/.local/share/xsessions") paths.push(homeDir + "/.local/share/xsessions");
} }
// Add XDG_DATA_DIRS paths // Add XDG_DATA_DIRS paths
if (xdgDirs) { if (xdgDirs) {
xdgDirs.split(":").forEach(function(dir) { xdgDirs.split(":").forEach(function (dir) {
if (dir) { if (dir) {
paths.push(dir + "/wayland-sessions") paths.push(dir + "/wayland-sessions");
paths.push(dir + "/xsessions") paths.push(dir + "/xsessions");
} }
}) });
} }
// 1. Explicit system/user paths // 1. Explicit system/user paths
var explicitFind = "find " + paths.join(" ") + " -maxdepth 1 -name '*.desktop' -type f -follow 2>/dev/null" var explicitFind = "find " + paths.join(" ") + " -maxdepth 1 -name '*.desktop' -type f -follow 2>/dev/null";
// 2. Scan all /home user directories for local session files // 2. Scan all /home user directories for local session files
var homeScan = "find /home -maxdepth 5 \\( -path '*/wayland-sessions/*.desktop' -o -path '*/xsessions/*.desktop' \\) -type f -follow 2>/dev/null" var homeScan = "find /home -maxdepth 5 \\( -path '*/wayland-sessions/*.desktop' -o -path '*/xsessions/*.desktop' \\) -type f -follow 2>/dev/null";
var findCmd = "(" + explicitFind + "; " + homeScan + ") | sort -u" var findCmd = "(" + explicitFind + "; " + homeScan + ") | sort -u";
return ["sh", "-c", findCmd] return ["sh", "-c", findCmd];
} }
running: false running: false
stdout: SplitParser { stdout: SplitParser {
onRead: data => { onRead: data => {
if (data.trim()) { if (data.trim()) {
root.pendingParsers++ root.pendingParsers++;
parseDesktopFile(data.trim()) parseDesktopFile(data.trim());
} }
} }
} }
@@ -1132,7 +1107,7 @@ Item {
function parseDesktopFile(path) { function parseDesktopFile(path) {
const parser = desktopParser.createObject(null, { const parser = desktopParser.createObject(null, {
"desktopPath": path "desktopPath": path
}) });
} }
Component { Component {
@@ -1144,41 +1119,40 @@ Item {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const lines = text.split("\n") const lines = text.split("\n");
let name = "" let name = "";
let exec = "" let exec = "";
for (const line of lines) { for (const line of lines) {
if (line.startsWith("Name=")) { if (line.startsWith("Name=")) {
name = line.substring(5).trim() name = line.substring(5).trim();
} else if (line.startsWith("Exec=")) { } else if (line.startsWith("Exec=")) {
exec = line.substring(5).trim() exec = line.substring(5).trim();
} }
} }
if (name && exec) { if (name && exec) {
if (!GreeterState.sessionList.includes(name)) { if (!GreeterState.sessionList.includes(name)) {
let newList = GreeterState.sessionList.slice() let newList = GreeterState.sessionList.slice();
let newExecs = GreeterState.sessionExecs.slice() let newExecs = GreeterState.sessionExecs.slice();
let newPaths = GreeterState.sessionPaths.slice() let newPaths = GreeterState.sessionPaths.slice();
newList.push(name) newList.push(name);
newExecs.push(exec) newExecs.push(exec);
newPaths.push(desktopPath) newPaths.push(desktopPath);
GreeterState.sessionList = newList GreeterState.sessionList = newList;
GreeterState.sessionExecs = newExecs GreeterState.sessionExecs = newExecs;
GreeterState.sessionPaths = newPaths GreeterState.sessionPaths = newPaths;
root.sessionCount = GreeterState.sessionList.length
} }
} }
} }
} }
onExited: code => { onExited: code => {
root.pendingParsers-- root.pendingParsers--;
if (root.pendingParsers === 0) { if (root.pendingParsers === 0) {
Qt.callLater(root.finalizeSessionSelection) Qt.callLater(root.finalizeSessionSelection);
} }
destroy() destroy();
} }
} }
} }
@@ -1189,34 +1163,34 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) { function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) { if (responseRequired) {
Greetd.respond(GreeterState.passwordBuffer) Greetd.respond(GreeterState.passwordBuffer);
GreeterState.passwordBuffer = "" GreeterState.passwordBuffer = "";
inputField.text = "" inputField.text = "";
} else if (!error) { } else if (!error) {
Greetd.respond("") Greetd.respond("");
} }
} }
function onReadyToLaunch() { function onReadyToLaunch() {
GreeterState.unlocking = true GreeterState.unlocking = true;
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex] const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
if (sessionCmd) { if (sessionCmd) {
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex]) GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex]);
GreetdMemory.setLastSuccessfulUser(GreeterState.username) GreetdMemory.setLastSuccessfulUser(GreeterState.username);
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]) Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
} }
} }
function onAuthFailure(message) { function onAuthFailure(message) {
GreeterState.pamState = "fail" GreeterState.pamState = "fail";
GreeterState.passwordBuffer = "" GreeterState.passwordBuffer = "";
inputField.text = "" inputField.text = "";
placeholderDelay.restart() placeholderDelay.restart();
} }
function onError(error) { function onError(error) {
GreeterState.pamState = "error" GreeterState.pamState = "error";
placeholderDelay.restart() placeholderDelay.restart();
} }
} }
@@ -1231,7 +1205,7 @@ Item {
showLogout: false showLogout: false
onClosed: { onClosed: {
if (isPrimaryScreen && inputField && inputField.forceActiveFocus) { if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
Qt.callLater(() => inputField.forceActiveFocus()) Qt.callLater(() => inputField.forceActiveFocus());
} }
} }
} }
+12 -8
View File
@@ -212,10 +212,12 @@ Item {
color: "transparent" color: "transparent"
Item { Item {
anchors.centerIn: parent id: clockContainer
anchors.verticalCenterOffset: -100 anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.verticalCenter
anchors.bottomMargin: 60
width: parent.width width: parent.width
height: 140 height: clockText.implicitHeight
visible: SettingsData.lockScreenShowTime visible: SettingsData.lockScreenShowTime
Row { Row {
@@ -330,8 +332,10 @@ Item {
} }
StyledText { StyledText {
anchors.centerIn: parent id: dateText
anchors.verticalCenterOffset: -25 anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockContainer.bottom
anchors.topMargin: 4
visible: SettingsData.lockScreenShowDate visible: SettingsData.lockScreenShowDate
text: { text: {
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) { if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
@@ -346,8 +350,9 @@ Item {
ColumnLayout { ColumnLayout {
id: passwordLayout id: passwordLayout
anchors.centerIn: parent anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenterOffset: 50 anchors.top: dateText.visible ? dateText.bottom : clockContainer.bottom
anchors.topMargin: Theme.spacingL
spacing: Theme.spacingM spacing: Theme.spacingM
width: 380 width: 380
@@ -384,7 +389,6 @@ Item {
border.width: passwordField.activeFocus ? 2 : 1 border.width: passwordField.activeFocus ? 2 : 1
visible: SettingsData.lockScreenShowPasswordField || root.passwordBuffer.length > 0 visible: SettingsData.lockScreenShowPasswordField || root.passwordBuffer.length > 0
Item { Item {
id: lockIconContainer id: lockIconContainer
anchors.left: parent.left anchors.left: parent.left
@@ -9,6 +9,7 @@ DankListView {
property var keyboardController: null property var keyboardController: null
property bool keyboardActive: false property bool keyboardActive: false
property bool autoScrollDisabled: false property bool autoScrollDisabled: false
property bool isAnimatingExpansion: false
property alias count: listView.count property alias count: listView.count
property alias listContentHeight: listView.contentHeight property alias listContentHeight: listView.contentHeight
@@ -29,8 +30,19 @@ DankListView {
Timer { Timer {
id: positionPreservationTimer id: positionPreservationTimer
interval: 200 interval: 200
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion
repeat: true repeat: true
onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion) {
keyboardController.ensureVisible();
}
}
}
Timer {
id: expansionEnsureVisibleTimer
interval: Theme.mediumDuration + 50
repeat: false
onTriggered: { onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) { if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
keyboardController.ensureVisible(); keyboardController.ensureVisible();
@@ -68,14 +80,7 @@ DankListView {
width: ListView.view.width width: ListView.view.width
height: isDismissing ? 0 : notificationCard.height height: isDismissing ? 0 : notificationCard.height
clip: true clip: isDismissing
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
NotificationCard { NotificationCard {
id: notificationCard id: notificationCard
@@ -84,6 +89,23 @@ DankListView {
notificationGroup: modelData notificationGroup: modelData
keyboardNavigationActive: listView.keyboardActive keyboardNavigationActive: listView.keyboardActive
opacity: 1 - Math.abs(delegateRoot.swipeOffset) / (delegateRoot.width * 0.5) opacity: 1 - Math.abs(delegateRoot.swipeOffset) / (delegateRoot.width * 0.5)
onIsAnimatingChanged: {
if (isAnimating) {
listView.isAnimatingExpansion = true;
} else {
Qt.callLater(() => {
let anyAnimating = false;
for (let i = 0; i < listView.count; i++) {
const item = listView.itemAtIndex(i);
if (item && item.children[0] && item.children[0].isAnimating) {
anyAnimating = true;
break;
}
}
listView.isAnimatingExpansion = anyAnimating;
});
}
}
isGroupSelected: { isGroupSelected: {
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive)
@@ -173,23 +195,15 @@ DankListView {
} }
function onExpandedGroupsChanged() { function onExpandedGroupsChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) { if (!keyboardController || !keyboardController.keyboardNavigationActive)
Qt.callLater(() => { return;
if (!autoScrollDisabled) { expansionEnsureVisibleTimer.restart();
keyboardController.ensureVisible();
}
});
}
} }
function onExpandedMessagesChanged() { function onExpandedMessagesChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) { if (!keyboardController || !keyboardController.keyboardNavigationActive)
Qt.callLater(() => { return;
if (!autoScrollDisabled) { expansionEnsureVisibleTimer.restart();
keyboardController.ensureVisible();
}
});
}
} }
} }
} }
@@ -1,8 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -13,9 +10,9 @@ Rectangle {
property var notificationGroup property var notificationGroup
property bool expanded: (NotificationService.expandedGroups[notificationGroup && notificationGroup.key] || false) property bool expanded: (NotificationService.expandedGroups[notificationGroup && notificationGroup.key] || false)
property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false)
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false)
property bool userInitiatedExpansion: false property bool userInitiatedExpansion: false
property bool isAnimating: false
property bool isGroupSelected: false property bool isGroupSelected: false
property int selectedNotificationIndex: -1 property int selectedNotificationIndex: -1
@@ -24,13 +21,13 @@ Rectangle {
width: parent ? parent.width : 400 width: parent ? parent.width : 400
height: { height: {
if (expanded) { if (expanded) {
return expandedContent.height + 28 return expandedContent.height + 28;
} }
const baseHeight = 116 const baseHeight = 116;
if (descriptionExpanded) { if (descriptionExpanded) {
return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2) return baseHeight + descriptionText.contentHeight - (descriptionText.font.pixelSize * 1.2 * 2);
} }
return baseHeight return baseHeight;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -43,36 +40,36 @@ Rectangle {
color: { color: {
if (isGroupSelected && keyboardNavigationActive) { if (isGroupSelected && keyboardNavigationActive) {
return Theme.primaryPressed return Theme.primaryPressed;
} }
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Theme.primaryHoverLight return Theme.primaryHoverLight;
} }
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
} }
border.color: { border.color: {
if (isGroupSelected && keyboardNavigationActive) { if (isGroupSelected && keyboardNavigationActive) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5);
} }
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
} }
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
} }
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05);
} }
border.width: { border.width: {
if (isGroupSelected && keyboardNavigationActive) { if (isGroupSelected && keyboardNavigationActive) {
return 1.5 return 1.5;
} }
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return 1 return 1;
} }
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return 2 return 2;
} }
return 1 return 1;
} }
clip: true clip: true
@@ -121,21 +118,21 @@ Rectangle {
imageSource: { imageSource: {
if (hasNotificationImage) if (hasNotificationImage)
return notificationGroup.latestNotification.cleanImage return notificationGroup.latestNotification.cleanImage;
if (notificationGroup?.latestNotification?.appIcon) { if (notificationGroup?.latestNotification?.appIcon) {
const appIcon = notificationGroup.latestNotification.appIcon const appIcon = notificationGroup.latestNotification.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://")) if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon return appIcon;
return Quickshell.iconPath(appIcon, true) return Quickshell.iconPath(appIcon, true);
} }
return "" return "";
} }
hasImage: hasNotificationImage hasImage: hasNotificationImage
fallbackIcon: "" fallbackIcon: ""
fallbackText: { fallbackText: {
const appName = notificationGroup?.appName || "?" const appName = notificationGroup?.appName || "?";
return appName.charAt(0).toUpperCase() return appName.charAt(0).toUpperCase();
} }
Rectangle { Rectangle {
@@ -195,9 +192,9 @@ Rectangle {
StyledText { StyledText {
width: parent.width width: parent.width
text: { text: {
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "" const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "";
const appName = (notificationGroup && notificationGroup.appName) || "" const appName = (notificationGroup && notificationGroup.appName) || "";
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName return timeStr.length > 0 ? `${appName} ${timeStr}` : appName;
} }
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -239,21 +236,20 @@ Rectangle {
onClicked: mouse => { onClicked: mouse => {
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) { if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "";
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "" NotificationService.toggleMessageExpansion(messageId);
NotificationService.toggleMessageExpansion(messageId)
} }
} }
propagateComposedEvents: true propagateComposedEvents: true
onPressed: mouse => { onPressed: mouse => {
if (parent.hoveredLink) { if (parent.hoveredLink) {
mouse.accepted = false mouse.accepted = false;
} }
} }
onReleased: mouse => { onReleased: mouse => {
if (parent.hoveredLink) { if (parent.hoveredLink) {
mouse.accepted = false mouse.accepted = false;
} }
} }
} }
@@ -335,15 +331,15 @@ Rectangle {
width: parent.width width: parent.width
height: { height: {
const baseHeight = 120 const baseHeight = 120;
if (messageExpanded) { if (messageExpanded) {
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2 const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
if (bodyText.implicitHeight > twoLineHeight + 2) { if (bodyText.implicitHeight > twoLineHeight + 2) {
const extraHeight = bodyText.implicitHeight - twoLineHeight const extraHeight = bodyText.implicitHeight - twoLineHeight;
return baseHeight + extraHeight return baseHeight + extraHeight;
} }
} }
return baseHeight return baseHeight;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -379,23 +375,23 @@ Rectangle {
imageSource: { imageSource: {
if (hasNotificationImage) if (hasNotificationImage)
return modelData.cleanImage return modelData.cleanImage;
if (modelData?.appIcon) { if (modelData?.appIcon) {
const appIcon = modelData.appIcon const appIcon = modelData.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://")) if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon return appIcon;
return Quickshell.iconPath(appIcon, true) return Quickshell.iconPath(appIcon, true);
} }
return "" return "";
} }
fallbackIcon: "" fallbackIcon: ""
fallbackText: { fallbackText: {
const appName = modelData?.appName || "?" const appName = modelData?.appName || "?";
return appName.charAt(0).toUpperCase() return appName.charAt(0).toUpperCase();
} }
} }
@@ -457,19 +453,19 @@ Rectangle {
onClicked: mouse => { onClicked: mouse => {
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) { if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "") NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
} }
} }
propagateComposedEvents: true propagateComposedEvents: true
onPressed: mouse => { onPressed: mouse => {
if (parent.hoveredLink) { if (parent.hoveredLink) {
mouse.accepted = false mouse.accepted = false;
} }
} }
onReleased: mouse => { onReleased: mouse => {
if (parent.hoveredLink) { if (parent.hoveredLink) {
mouse.accepted = false mouse.accepted = false;
} }
} }
} }
@@ -502,11 +498,11 @@ Rectangle {
StyledText { StyledText {
id: actionText id: actionText
text: { text: {
const baseText = modelData.text || "View" const baseText = modelData.text || "View";
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) { if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) {
return `${baseText} (${index + 1})` return `${baseText} (${index + 1})`;
} }
return baseText return baseText;
} }
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -523,7 +519,7 @@ Rectangle {
onExited: parent.isHovered = false onExited: parent.isHovered = false
onClicked: { onClicked: {
if (modelData && modelData.invoke) { if (modelData && modelData.invoke) {
modelData.invoke() modelData.invoke();
} }
} }
} }
@@ -587,11 +583,11 @@ Rectangle {
StyledText { StyledText {
id: actionText id: actionText
text: { text: {
const baseText = modelData.text || "View" const baseText = modelData.text || "View";
if (keyboardNavigationActive && isGroupSelected) { if (keyboardNavigationActive && isGroupSelected) {
return `${baseText} (${index + 1})` return `${baseText} (${index + 1})`;
} }
return baseText return baseText;
} }
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -608,7 +604,7 @@ Rectangle {
onExited: parent.isHovered = false onExited: parent.isHovered = false
onClicked: { onClicked: {
if (modelData && modelData.invoke) { if (modelData && modelData.invoke) {
modelData.invoke() modelData.invoke();
} }
} }
} }
@@ -654,8 +650,8 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
onClicked: { onClicked: {
root.userInitiatedExpansion = true root.userInitiatedExpansion = true;
NotificationService.toggleGroupExpansion(notificationGroup?.key || "") NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
} }
z: -1 z: -1
} }
@@ -677,8 +673,8 @@ Rectangle {
iconSize: 18 iconSize: 18
buttonSize: 28 buttonSize: 28
onClicked: { onClicked: {
root.userInitiatedExpansion = true root.userInitiatedExpansion = true;
NotificationService.toggleGroupExpansion(notificationGroup?.key || "") NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
} }
} }
@@ -697,7 +693,14 @@ Rectangle {
NumberAnimation { NumberAnimation {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
onFinished: root.userInitiatedExpansion = false onRunningChanged: {
if (running) {
root.isAnimating = true;
} else {
root.isAnimating = false;
root.userInitiatedExpansion = false;
}
}
} }
} }
} }
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import qs.Common
import qs.Services import qs.Services
QtObject { QtObject {
@@ -23,14 +22,14 @@ QtObject {
property bool isRebuilding: false property bool isRebuilding: false
function rebuildFlatNavigation() { function rebuildFlatNavigation() {
isRebuilding = true isRebuilding = true;
const nav = [] const nav = [];
const groups = NotificationService.groupedNotifications const groups = NotificationService.groupedNotifications;
for (var i = 0; i < groups.length; i++) { for (var i = 0; i < groups.length; i++) {
const group = groups[i] const group = groups[i];
const isExpanded = NotificationService.expandedGroups[group.key] || false const isExpanded = NotificationService.expandedGroups[group.key] || false;
nav.push({ nav.push({
"type": "group", "type": "group",
@@ -38,515 +37,503 @@ QtObject {
"notificationIndex": -1, "notificationIndex": -1,
"groupKey": group.key, "groupKey": group.key,
"notificationId": "" "notificationId": ""
}) });
if (isExpanded) { if (isExpanded) {
const notifications = group.notifications || [] const notifications = group.notifications || [];
const maxNotifications = Math.min(notifications.length, 10) const maxNotifications = Math.min(notifications.length, 10);
for (var j = 0; j < maxNotifications; j++) { for (var j = 0; j < maxNotifications; j++) {
const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : "") const notifId = String(notifications[j] && notifications[j].notification && notifications[j].notification.id ? notifications[j].notification.id : "");
nav.push({ nav.push({
"type": "notification", "type": "notification",
"groupIndex": i, "groupIndex": i,
"notificationIndex": j, "notificationIndex": j,
"groupKey": group.key, "groupKey": group.key,
"notificationId": notifId "notificationId": notifId
}) });
} }
} }
} }
flatNavigation = nav flatNavigation = nav;
updateSelectedIndexFromId() updateSelectedIndexFromId();
isRebuilding = false isRebuilding = false;
} }
function updateSelectedIndexFromId() { function updateSelectedIndexFromId() {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
return return;
} }
for (var i = 0; i < flatNavigation.length; i++) { for (var i = 0; i < flatNavigation.length; i++) {
const item = flatNavigation[i] const item = flatNavigation[i];
if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) { if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) {
selectedFlatIndex = i selectedFlatIndex = i;
selectionVersion++ // Trigger UI update selectionVersion++; // Trigger UI update
return return;
} else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) { } else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) {
selectedFlatIndex = i selectedFlatIndex = i;
selectionVersion++ // Trigger UI update selectionVersion++; // Trigger UI update
return return;
} }
} }
// If not found, try to find the same group but select the group header instead // If not found, try to find the same group but select the group header instead
if (selectedItemType === "notification") { if (selectedItemType === "notification") {
for (var j = 0; j < flatNavigation.length; j++) { for (var j = 0; j < flatNavigation.length; j++) {
const groupItem = flatNavigation[j] const groupItem = flatNavigation[j];
if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) { if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) {
selectedFlatIndex = j selectedFlatIndex = j;
selectedItemType = "group" selectedItemType = "group";
selectedNotificationId = "" selectedNotificationId = "";
selectionVersion++ // Trigger UI update selectionVersion++; // Trigger UI update
return return;
} }
} }
} }
// If still not found, clamp to valid range and update // If still not found, clamp to valid range and update
if (flatNavigation.length > 0) { if (flatNavigation.length > 0) {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1);
selectedFlatIndex = Math.max(selectedFlatIndex, 0) selectedFlatIndex = Math.max(selectedFlatIndex, 0);
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
selectionVersion++ // Trigger UI update selectionVersion++; // Trigger UI update
} }
} }
function updateSelectedIdFromIndex() { function updateSelectedIdFromIndex() {
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) { if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) {
const item = flatNavigation[selectedFlatIndex] const item = flatNavigation[selectedFlatIndex];
selectedItemType = item.type selectedItemType = item.type;
selectedGroupKey = item.groupKey selectedGroupKey = item.groupKey;
selectedNotificationId = item.notificationId selectedNotificationId = item.notificationId;
} }
} }
function reset() { function reset() {
selectedFlatIndex = 0 selectedFlatIndex = 0;
keyboardNavigationActive = false keyboardNavigationActive = false;
showKeyboardHints = false showKeyboardHints = false;
// Reset keyboardActive when modal is reset // Reset keyboardActive when modal is reset
if (listView) { if (listView) {
listView.keyboardActive = false listView.keyboardActive = false;
} }
rebuildFlatNavigation() rebuildFlatNavigation();
} }
function selectNext() { function selectNext() {
keyboardNavigationActive = true keyboardNavigationActive = true;
if (flatNavigation.length === 0) if (flatNavigation.length === 0)
return return;
// Re-enable auto-scrolling when arrow keys are used // Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) { if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll() listView.enableAutoScroll();
} }
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1) selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1);
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
} }
function selectNextWrapping() { function selectNextWrapping() {
keyboardNavigationActive = true keyboardNavigationActive = true;
if (flatNavigation.length === 0) if (flatNavigation.length === 0)
return return;
// Re-enable auto-scrolling when arrow keys are used // Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) { if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll() listView.enableAutoScroll();
} }
selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
} }
function selectPrevious() { function selectPrevious() {
keyboardNavigationActive = true keyboardNavigationActive = true;
if (flatNavigation.length === 0) if (flatNavigation.length === 0)
return return;
// Re-enable auto-scrolling when arrow keys are used // Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) { if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll() listView.enableAutoScroll();
} }
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0) selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0);
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
} }
function toggleGroupExpanded() { function toggleGroupExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return return;
const currentItem = flatNavigation[selectedFlatIndex];
const currentItem = flatNavigation[selectedFlatIndex] const groups = NotificationService.groupedNotifications;
const groups = NotificationService.groupedNotifications const group = groups[currentItem.groupIndex];
const group = groups[currentItem.groupIndex]
if (!group) if (!group)
return return;
// Prevent expanding groups with < 2 notifications // Prevent expanding groups with < 2 notifications
const notificationCount = group.notifications ? group.notifications.length : 0 const notificationCount = group.notifications ? group.notifications.length : 0;
if (notificationCount < 2) if (notificationCount < 2)
return return;
const wasExpanded = NotificationService.expandedGroups[group.key] || false;
const groupIndex = currentItem.groupIndex;
const wasExpanded = NotificationService.expandedGroups[group.key] || false isTogglingGroup = true;
const groupIndex = currentItem.groupIndex NotificationService.toggleGroupExpansion(group.key);
rebuildFlatNavigation();
isTogglingGroup = true
NotificationService.toggleGroupExpansion(group.key)
rebuildFlatNavigation()
// Smart selection after toggle // Smart selection after toggle
if (!wasExpanded) { if (!wasExpanded) {
// Just expanded - move to first notification in the group // Just expanded - move to first notification in the group
for (var i = 0; i < flatNavigation.length; i++) { for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) { if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i selectedFlatIndex = i;
break break;
} }
} }
} else { } else {
// Just collapsed - stay on the group header // Just collapsed - stay on the group header
for (var i = 0; i < flatNavigation.length; i++) { for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) { if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i selectedFlatIndex = i;
break break;
} }
} }
} }
isTogglingGroup = false isTogglingGroup = false;
ensureVisible()
} }
function handleEnterKey() { function handleEnterKey() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return return;
const currentItem = flatNavigation[selectedFlatIndex];
const currentItem = flatNavigation[selectedFlatIndex] const groups = NotificationService.groupedNotifications;
const groups = NotificationService.groupedNotifications const group = groups[currentItem.groupIndex];
const group = groups[currentItem.groupIndex]
if (!group) if (!group)
return return;
if (currentItem.type === "group") { if (currentItem.type === "group") {
const notificationCount = group.notifications ? group.notifications.length : 0 const notificationCount = group.notifications ? group.notifications.length : 0;
if (notificationCount >= 2) { if (notificationCount >= 2) {
toggleGroupExpanded() toggleGroupExpanded();
} else { } else {
executeAction(0) executeAction(0);
} }
} else if (currentItem.type === "notification") { } else if (currentItem.type === "notification") {
executeAction(0) executeAction(0);
} }
} }
function toggleTextExpanded() { function toggleTextExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return return;
const currentItem = flatNavigation[selectedFlatIndex];
const currentItem = flatNavigation[selectedFlatIndex] const groups = NotificationService.groupedNotifications;
const groups = NotificationService.groupedNotifications const group = groups[currentItem.groupIndex];
const group = groups[currentItem.groupIndex]
if (!group) if (!group)
return return;
let messageId = "";
let messageId = ""
if (currentItem.type === "group") { if (currentItem.type === "group") {
messageId = group.latestNotification?.notification?.id + "_desc" messageId = group.latestNotification?.notification?.id + "_desc";
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { } else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) {
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc" messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc";
} }
if (messageId) { if (messageId) {
NotificationService.toggleMessageExpansion(messageId) NotificationService.toggleMessageExpansion(messageId);
} }
} }
function executeAction(actionIndex) { function executeAction(actionIndex) {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return return;
const currentItem = flatNavigation[selectedFlatIndex];
const currentItem = flatNavigation[selectedFlatIndex] const groups = NotificationService.groupedNotifications;
const groups = NotificationService.groupedNotifications const group = groups[currentItem.groupIndex];
const group = groups[currentItem.groupIndex]
if (!group) if (!group)
return return;
let actions = [];
let actions = []
if (currentItem.type === "group") { if (currentItem.type === "group") {
actions = group.latestNotification?.actions || [] actions = group.latestNotification?.actions || [];
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { } else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) {
actions = group.notifications[currentItem.notificationIndex]?.actions || [] actions = group.notifications[currentItem.notificationIndex]?.actions || [];
} }
if (actionIndex >= 0 && actionIndex < actions.length) { if (actionIndex >= 0 && actionIndex < actions.length) {
const action = actions[actionIndex] const action = actions[actionIndex];
if (action.invoke) { if (action.invoke) {
action.invoke() action.invoke();
if (onClose) if (onClose)
onClose() onClose();
} }
} }
} }
function clearSelected() { function clearSelected() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return return;
const currentItem = flatNavigation[selectedFlatIndex];
const currentItem = flatNavigation[selectedFlatIndex] const groups = NotificationService.groupedNotifications;
const groups = NotificationService.groupedNotifications const group = groups[currentItem.groupIndex];
const group = groups[currentItem.groupIndex]
if (!group) if (!group)
return return;
if (currentItem.type === "group") { if (currentItem.type === "group") {
NotificationService.dismissGroup(group.key) NotificationService.dismissGroup(group.key);
} else if (currentItem.type === "notification") { } else if (currentItem.type === "notification") {
const notification = group.notifications[currentItem.notificationIndex] const notification = group.notifications[currentItem.notificationIndex];
NotificationService.dismissNotification(notification) NotificationService.dismissNotification(notification);
} }
rebuildFlatNavigation() rebuildFlatNavigation();
if (flatNavigation.length === 0) { if (flatNavigation.length === 0) {
keyboardNavigationActive = false keyboardNavigationActive = false;
if (listView) { if (listView) {
listView.keyboardActive = false listView.keyboardActive = false;
} }
} else { } else {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1);
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
ensureVisible() ensureVisible();
} }
} }
function findRepeater(parent) { function findRepeater(parent) {
if (!parent || !parent.children) { if (!parent || !parent.children) {
return null return null;
} }
for (var i = 0; i < parent.children.length; i++) { for (var i = 0; i < parent.children.length; i++) {
const child = parent.children[i] const child = parent.children[i];
if (child.objectName === "notificationRepeater") { if (child.objectName === "notificationRepeater") {
return child return child;
} }
const found = findRepeater(child) const found = findRepeater(child);
if (found) { if (found) {
return found return found;
} }
} }
return null return null;
} }
function ensureVisible() { function ensureVisible() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView) if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView)
return return;
const currentItem = flatNavigation[selectedFlatIndex];
const currentItem = flatNavigation[selectedFlatIndex]
if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) { if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) {
if (currentItem.type === "notification") { if (currentItem.type === "notification") {
const groupDelegate = listView.itemAtIndex(currentItem.groupIndex) const groupDelegate = listView.itemAtIndex(currentItem.groupIndex);
if (groupDelegate && groupDelegate.children && groupDelegate.children.length > 0) { if (groupDelegate && groupDelegate.children && groupDelegate.children.length > 0) {
const notificationCard = groupDelegate.children[0] const notificationCard = groupDelegate.children[0];
const repeater = findRepeater(notificationCard) const repeater = findRepeater(notificationCard);
if (repeater && currentItem.notificationIndex < repeater.count) { if (repeater && currentItem.notificationIndex < repeater.count) {
const notificationItem = repeater.itemAt(currentItem.notificationIndex) const notificationItem = repeater.itemAt(currentItem.notificationIndex);
if (notificationItem) { if (notificationItem) {
const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0) const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0);
const itemY = itemPos.y const itemY = itemPos.y;
const itemHeight = notificationItem.height const itemHeight = notificationItem.height;
const viewportTop = listView.contentY const viewportTop = listView.contentY;
const viewportBottom = listView.contentY + listView.height const viewportBottom = listView.contentY + listView.height;
if (itemY < viewportTop) { if (itemY < viewportTop) {
listView.contentY = itemY - 20 listView.contentY = itemY - 20;
} else if (itemY + itemHeight > viewportBottom) { } else if (itemY + itemHeight > viewportBottom) {
listView.contentY = itemY + itemHeight - listView.height + 20 listView.contentY = itemY + itemHeight - listView.height + 20;
} }
} }
} }
} }
} else { } else {
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain) listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain);
} }
listView.forceLayout() listView.forceLayout();
} }
} }
function handleKey(event) { function handleKey(event) {
if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) { if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) {
NotificationService.clearAllNotifications() NotificationService.clearAllNotifications();
rebuildFlatNavigation() rebuildFlatNavigation();
if (flatNavigation.length === 0) { if (flatNavigation.length === 0) {
keyboardNavigationActive = false keyboardNavigationActive = false;
if (listView) { if (listView) {
listView.keyboardActive = false listView.keyboardActive = false;
} }
} else { } else {
selectedFlatIndex = 0 selectedFlatIndex = 0;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
} }
selectionVersion++ selectionVersion++;
event.accepted = true event.accepted = true;
return return;
} }
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (keyboardNavigationActive) { if (keyboardNavigationActive) {
keyboardNavigationActive = false keyboardNavigationActive = false;
event.accepted = true event.accepted = true;
} else { } else {
if (onClose) if (onClose)
onClose() onClose();
event.accepted = true event.accepted = true;
} }
} else if (event.key === Qt.Key_Down || event.key === 16777237) { } else if (event.key === Qt.Key_Down || event.key === 16777237) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true;
rebuildFlatNavigation() // Ensure we have fresh navigation data rebuildFlatNavigation(); // Ensure we have fresh navigation data
selectedFlatIndex = 0 selectedFlatIndex = 0;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
// Set keyboardActive on listView to show highlight // Set keyboardActive on listView to show highlight
if (listView) { if (listView) {
listView.keyboardActive = true listView.keyboardActive = true;
} }
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
event.accepted = true event.accepted = true;
} else { } else {
selectNext() selectNext();
event.accepted = true event.accepted = true;
} }
} else if (event.key === Qt.Key_Up || event.key === 16777235) { } else if (event.key === Qt.Key_Up || event.key === 16777235) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true;
rebuildFlatNavigation() // Ensure we have fresh navigation data rebuildFlatNavigation(); // Ensure we have fresh navigation data
selectedFlatIndex = 0 selectedFlatIndex = 0;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
// Set keyboardActive on listView to show highlight // Set keyboardActive on listView to show highlight
if (listView) { if (listView) {
listView.keyboardActive = true listView.keyboardActive = true;
} }
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
event.accepted = true event.accepted = true;
} else if (selectedFlatIndex === 0) { } else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false keyboardNavigationActive = false;
// Reset keyboardActive when navigation is disabled // Reset keyboardActive when navigation is disabled
if (listView) { if (listView) {
listView.keyboardActive = false listView.keyboardActive = false;
} }
selectionVersion++ selectionVersion++;
event.accepted = true event.accepted = true;
return return;
} else { } else {
selectPrevious() selectPrevious();
event.accepted = true event.accepted = true;
} }
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) { } else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true;
rebuildFlatNavigation() rebuildFlatNavigation();
selectedFlatIndex = 0 selectedFlatIndex = 0;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
if (listView) { if (listView) {
listView.keyboardActive = true listView.keyboardActive = true;
} }
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
} else { } else {
selectNext() selectNext();
} }
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) { } else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true;
rebuildFlatNavigation() rebuildFlatNavigation();
selectedFlatIndex = 0 selectedFlatIndex = 0;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
if (listView) { if (listView) {
listView.keyboardActive = true listView.keyboardActive = true;
} }
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
} else if (selectedFlatIndex === 0) { } else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false keyboardNavigationActive = false;
if (listView) { if (listView) {
listView.keyboardActive = false listView.keyboardActive = false;
} }
selectionVersion++ selectionVersion++;
} else { } else {
selectPrevious() selectPrevious();
} }
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) { } else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true;
rebuildFlatNavigation() rebuildFlatNavigation();
selectedFlatIndex = 0 selectedFlatIndex = 0;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
if (listView) { if (listView) {
listView.keyboardActive = true listView.keyboardActive = true;
} }
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
} else { } else {
selectNext() selectNext();
} }
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) { } else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true;
rebuildFlatNavigation() rebuildFlatNavigation();
selectedFlatIndex = 0 selectedFlatIndex = 0;
updateSelectedIdFromIndex() updateSelectedIdFromIndex();
if (listView) { if (listView) {
listView.keyboardActive = true listView.keyboardActive = true;
} }
selectionVersion++ selectionVersion++;
ensureVisible() ensureVisible();
} else if (selectedFlatIndex === 0) { } else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false keyboardNavigationActive = false;
if (listView) { if (listView) {
listView.keyboardActive = false listView.keyboardActive = false;
} }
selectionVersion++ selectionVersion++;
} else { } else {
selectPrevious() selectPrevious();
} }
event.accepted = true event.accepted = true;
} else if (keyboardNavigationActive) { } else if (keyboardNavigationActive) {
if (event.key === Qt.Key_Space) { if (event.key === Qt.Key_Space) {
toggleGroupExpanded() toggleGroupExpanded();
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
handleEnterKey() handleEnterKey();
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_E) { } else if (event.key === Qt.Key_E) {
toggleTextExpanded() toggleTextExpanded();
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { } else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
clearSelected() clearSelected();
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Tab) { } else if (event.key === Qt.Key_Tab) {
selectNext() selectNext();
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_Backtab) { } else if (event.key === Qt.Key_Backtab) {
selectPrevious() selectPrevious();
event.accepted = true event.accepted = true;
} else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) { } else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
const actionIndex = event.key - Qt.Key_1 const actionIndex = event.key - Qt.Key_1;
executeAction(actionIndex) executeAction(actionIndex);
event.accepted = true event.accepted = true;
} }
} }
if (event.key === Qt.Key_F10) { if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints showKeyboardHints = !showKeyboardHints;
event.accepted = true event.accepted = true;
} }
} }
@@ -557,13 +544,13 @@ QtObject {
"type": "", "type": "",
"groupIndex": -1, "groupIndex": -1,
"notificationIndex": -1 "notificationIndex": -1
} };
} }
const result = flatNavigation[selectedFlatIndex] || { const result = flatNavigation[selectedFlatIndex] || {
"type": "", "type": "",
"groupIndex": -1, "groupIndex": -1,
"notificationIndex": -1 "notificationIndex": -1
} };
return result return result;
} }
} }
+78 -23
View File
@@ -13,31 +13,88 @@ Item {
property bool saving: false property bool saving: false
readonly property var maxHistoryOptions: [ readonly property var maxHistoryOptions: [
{ text: "25", value: 25 }, {
{ text: "50", value: 50 }, text: "25",
{ text: "100", value: 100 }, value: 25
{ text: "200", value: 200 }, },
{ text: "500", value: 500 }, {
{ text: "1000", value: 1000 } text: "50",
value: 50
},
{
text: "100",
value: 100
},
{
text: "200",
value: 200
},
{
text: "500",
value: 500
},
{
text: "1000",
value: 1000
}
] ]
readonly property var maxEntrySizeOptions: [ readonly property var maxEntrySizeOptions: [
{ text: "1 MB", value: 1048576 }, {
{ text: "2 MB", value: 2097152 }, text: "1 MB",
{ text: "5 MB", value: 5242880 }, value: 1048576
{ text: "10 MB", value: 10485760 }, },
{ text: "20 MB", value: 20971520 }, {
{ text: "50 MB", value: 52428800 } text: "2 MB",
value: 2097152
},
{
text: "5 MB",
value: 5242880
},
{
text: "10 MB",
value: 10485760
},
{
text: "20 MB",
value: 20971520
},
{
text: "50 MB",
value: 52428800
}
] ]
readonly property var autoClearOptions: [ readonly property var autoClearOptions: [
{ text: I18n.tr("Never"), value: 0 }, {
{ text: I18n.tr("1 day"), value: 1 }, text: I18n.tr("Never"),
{ text: I18n.tr("3 days"), value: 3 }, value: 0
{ text: I18n.tr("7 days"), value: 7 }, },
{ text: I18n.tr("14 days"), value: 14 }, {
{ text: I18n.tr("30 days"), value: 30 }, text: I18n.tr("1 day"),
{ text: I18n.tr("90 days"), value: 90 } value: 1
},
{
text: I18n.tr("3 days"),
value: 3
},
{
text: I18n.tr("7 days"),
value: 7
},
{
text: I18n.tr("14 days"),
value: 14
},
{
text: I18n.tr("30 days"),
value: 30
},
{
text: I18n.tr("90 days"),
value: 90
}
] ]
function getMaxHistoryText(value) { function getMaxHistoryText(value) {
@@ -139,9 +196,7 @@ Item {
StyledText { StyledText {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
text: !DMSService.isConnected text: !DMSService.isConnected ? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.") : I18n.tr("Failed to load clipboard configuration.")
? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.")
: I18n.tr("Failed to load clipboard configuration.")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width - Theme.iconSizeSmall - Theme.spacingM width: parent.width - Theme.iconSizeSmall - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -264,7 +319,7 @@ Item {
settingKey: "disablePersist" settingKey: "disablePersist"
text: I18n.tr("Disable Clipboard Ownership") text: I18n.tr("Disable Clipboard Ownership")
description: I18n.tr("Don't preserve clipboard when apps close") description: I18n.tr("Don't preserve clipboard when apps close")
checked: root.config.disablePersist ?? false checked: root.config.disablePersist ?? true
onToggled: checked => root.saveConfig("disablePersist", checked) onToggled: checked => root.saveConfig("disablePersist", checked)
} }
} }
+88 -1
View File
@@ -237,7 +237,10 @@ Item {
visible: defaultBar.visible ?? true, visible: defaultBar.visible ?? true,
popupGapsAuto: defaultBar.popupGapsAuto ?? true, popupGapsAuto: defaultBar.popupGapsAuto ?? true,
popupGapsManual: defaultBar.popupGapsManual ?? 4, popupGapsManual: defaultBar.popupGapsManual ?? 4,
maximizeDetection: defaultBar.maximizeDetection ?? true maximizeDetection: defaultBar.maximizeDetection ?? true,
scrollEnabled: defaultBar.scrollEnabled ?? true,
scrollXBehavior: defaultBar.scrollXBehavior ?? "column",
scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace"
}; };
SettingsData.addBarConfig(newBar); SettingsData.addBarConfig(newBar);
selectedBarId = newId; selectedBarId = newId;
@@ -751,6 +754,90 @@ Item {
}) })
} }
SettingsToggleCard {
iconName: "mouse"
title: I18n.tr("Scroll Wheel")
description: I18n.tr("Control workspaces and columns by scrolling on the bar")
visible: selectedBarConfig?.enabled
checked: selectedBarConfig?.scrollEnabled ?? true
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
scrollEnabled: checked
})
SettingsButtonGroupRow {
text: I18n.tr("Y Axis")
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
currentIndex: {
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
case "none":
return 0;
case "workspace":
return 1;
case "column":
return 2;
default:
return 1;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let behavior = "workspace";
switch (index) {
case 0:
behavior = "none";
break;
case 1:
behavior = "workspace";
break;
case 2:
behavior = "column";
break;
}
SettingsData.updateBarConfig(selectedBarId, {
scrollYBehavior: behavior
});
}
}
SettingsButtonGroupRow {
text: I18n.tr("X Axis")
visible: CompositorService.isNiri
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
currentIndex: {
switch (selectedBarConfig?.scrollXBehavior || "column") {
case "none":
return 0;
case "workspace":
return 1;
case "column":
return 2;
default:
return 2;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let behavior = "column";
switch (index) {
case 0:
behavior = "none";
break;
case 1:
behavior = "workspace";
break;
case 2:
behavior = "column";
break;
}
SettingsData.updateBarConfig(selectedBarId, {
scrollXBehavior: behavior
});
}
}
}
SettingsCard { SettingsCard {
iconName: "space_bar" iconName: "space_bar"
title: I18n.tr("Spacing") title: I18n.tr("Spacing")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,490 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
function getBarComponentsFromSettings() {
const bars = SettingsData.barConfigs || []
return bars.map(bar => ({
"id": "bar:" + bar.id,
"name": bar.name || "Bar",
"description": I18n.tr("Individual bar configuration"),
"icon": "toolbar",
"barId": bar.id
}))
}
property var variantComponents: getVariantComponentsList()
function getVariantComponentsList() {
return [...getBarComponentsFromSettings(), {
"id": "dock",
"name": I18n.tr("Application Dock"),
"description": I18n.tr("Bottom dock for pinned and running applications"),
"icon": "dock"
}, {
"id": "notifications",
"name": I18n.tr("Notification Popups"),
"description": I18n.tr("Notification toast popups"),
"icon": "notifications"
}, {
"id": "wallpaper",
"name": I18n.tr("Wallpaper"),
"description": I18n.tr("Desktop background images"),
"icon": "wallpaper"
}, {
"id": "osd",
"name": I18n.tr("On-Screen Displays"),
"description": I18n.tr("Volume, brightness, and other system OSDs"),
"icon": "picture_in_picture"
}, {
"id": "toast",
"name": I18n.tr("Toast Messages"),
"description": I18n.tr("System toast notifications"),
"icon": "campaign"
}, {
"id": "notepad",
"name": I18n.tr("Notepad Slideout"),
"description": I18n.tr("Quick note-taking slideout panel"),
"icon": "sticky_note_2"
}]
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
variantComponents = getVariantComponentsList()
}
}
function getScreenPreferences(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4)
const barConfig = SettingsData.getBarConfig(barId)
return barConfig?.screenPreferences || ["all"]
}
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"]
}
function setScreenPreferences(componentId, screenNames) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4)
SettingsData.updateBarConfig(barId, {
"screenPreferences": screenNames
})
return
}
var prefs = SettingsData.screenPreferences || {}
var newPrefs = Object.assign({}, prefs)
newPrefs[componentId] = screenNames
SettingsData.set("screenPreferences", newPrefs)
}
function getShowOnLastDisplay(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4)
const barConfig = SettingsData.getBarConfig(barId)
return barConfig?.showOnLastDisplay ?? true
}
return SettingsData.showOnLastDisplay && SettingsData.showOnLastDisplay[componentId] || false
}
function setShowOnLastDisplay(componentId, enabled) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4)
SettingsData.updateBarConfig(barId, {
"showOnLastDisplay": enabled
})
return
}
var prefs = SettingsData.showOnLastDisplay || {}
var newPrefs = Object.assign({}, prefs)
newPrefs[componentId] = enabled
SettingsData.set("showOnLastDisplay", newPrefs)
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: screensInfoSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Connected Displays")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Configure which displays show shell components")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Available Screens (") + Quickshell.screens.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Item {
width: 1
height: 1
Layout.fillWidth: true
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Display Name Format")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
DankButtonGroup {
id: displayModeGroup
model: [I18n.tr("Name"), I18n.tr("Model")]
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return
SettingsData.displayNameMode = index === 1 ? "model" : "system"
SettingsData.saveSettings()
}
Connections {
target: SettingsData
function onDisplayNameModeChanged() {
displayModeGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0
}
}
}
}
}
}
Repeater {
model: Quickshell.screens
delegate: Rectangle {
width: parent.width
height: screenRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 0
Row {
id: screenRow
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingM
DankIcon {
name: "desktop_windows"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS / 2
StyledText {
text: SettingsData.getScreenDisplayName(modelData)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingS
property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null
property var currentMode: wlrOutput?.currentMode
StyledText {
text: {
if (parent.currentMode) {
return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz"
}
return modelData.width + "×" + modelData.height
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingL
Repeater {
model: root.variantComponents
delegate: StyledRect {
width: parent.width
height: componentSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: componentSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Show on screens:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
property string componentId: modelData.id
width: parent.width
spacing: Theme.spacingXS
DankToggle {
width: parent.width
text: I18n.tr("All displays")
description: I18n.tr("Show on all connected displays")
checked: {
var prefs = root.getScreenPreferences(parent.componentId)
return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")
}
onToggled: checked => {
if (checked) {
root.setScreenPreferences(parent.componentId, ["all"])
} else {
root.setScreenPreferences(parent.componentId, [])
const cid = parent.componentId
if (["dankBar", "dock", "notifications", "osd", "toast"].includes(cid) || cid.startsWith("bar:")) {
root.setShowOnLastDisplay(cid, true)
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show on Last Display")
description: I18n.tr("Always show when there's only one connected display")
checked: root.getShowOnLastDisplay(parent.componentId)
visible: {
const prefs = root.getScreenPreferences(parent.componentId)
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")
const cid = parent.componentId
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:")
return !isAll && isRelevantComponent
}
onToggled: checked => {
root.setShowOnLastDisplay(parent.componentId, checked)
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: {
var prefs = root.getScreenPreferences(parent.componentId)
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all")
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: {
var prefs = root.getScreenPreferences(parent.componentId)
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all")
}
Repeater {
model: Quickshell.screens
delegate: DankToggle {
property var screenData: modelData
property string componentId: parent.parent.componentId
width: parent.width
text: SettingsData.getScreenDisplayName(screenData)
description: screenData.width + "×" + screenData.height + " • " + (SettingsData.displayNameMode === "system" ? (screenData.model || "Unknown Model") : screenData.name)
checked: {
var prefs = root.getScreenPreferences(componentId)
if (typeof prefs[0] === "string" && prefs[0] === "all")
return false
return SettingsData.isScreenInPreferences(screenData, prefs)
}
onToggled: checked => {
var currentPrefs = root.getScreenPreferences(componentId)
if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") {
currentPrefs = []
}
const screenModelIndex = SettingsData.getScreenModelIndex(screenData)
var newPrefs = currentPrefs.filter(pref => {
if (typeof pref === "string")
return false
if (pref.modelIndex !== undefined && screenModelIndex >= 0) {
return !(pref.model === screenData.model && pref.modelIndex === screenModelIndex)
}
return pref.name !== screenData.name || pref.model !== screenData.model
})
if (checked) {
const prefObj = {
"name": screenData.name,
"model": screenData.model || ""
}
if (screenModelIndex >= 0) {
prefObj.modelIndex = screenModelIndex
}
newPrefs.push(prefObj)
}
root.setScreenPreferences(componentId, newPrefs)
}
}
}
}
}
}
}
}
}
}
}
}
}
@@ -6,128 +6,21 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: displaysTab id: root
function formatGammaTime(isoString) { function formatGammaTime(isoString) {
if (!isoString) if (!isoString)
return ""; return ""
try { try {
const date = new Date(isoString); const date = new Date(isoString)
if (isNaN(date.getTime())) if (isNaN(date.getTime()))
return ""; return ""
return date.toLocaleTimeString(Qt.locale(), "HH:mm"); return date.toLocaleTimeString(Qt.locale(), "HH:mm")
} catch (e) { } catch (e) {
return ""; return ""
} }
} }
function getBarComponentsFromSettings() {
const bars = SettingsData.barConfigs || [];
return bars.map(bar => ({
"id": "bar:" + bar.id,
"name": bar.name || "Bar",
"description": I18n.tr("Individual bar configuration"),
"icon": "toolbar",
"barId": bar.id
}));
}
property var variantComponents: getVariantComponentsList()
function getVariantComponentsList() {
return [...getBarComponentsFromSettings(),
{
"id": "dock",
"name": I18n.tr("Application Dock"),
"description": I18n.tr("Bottom dock for pinned and running applications"),
"icon": "dock"
},
{
"id": "notifications",
"name": I18n.tr("Notification Popups"),
"description": I18n.tr("Notification toast popups"),
"icon": "notifications"
},
{
"id": "wallpaper",
"name": I18n.tr("Wallpaper"),
"description": I18n.tr("Desktop background images"),
"icon": "wallpaper"
},
{
"id": "osd",
"name": I18n.tr("On-Screen Displays"),
"description": I18n.tr("Volume, brightness, and other system OSDs"),
"icon": "picture_in_picture"
},
{
"id": "toast",
"name": I18n.tr("Toast Messages"),
"description": I18n.tr("System toast notifications"),
"icon": "campaign"
},
{
"id": "notepad",
"name": I18n.tr("Notepad Slideout"),
"description": I18n.tr("Quick note-taking slideout panel"),
"icon": "sticky_note_2"
},
];
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
variantComponents = getVariantComponentsList();
}
}
function getScreenPreferences(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
const barConfig = SettingsData.getBarConfig(barId);
return barConfig?.screenPreferences || ["all"];
}
return SettingsData.screenPreferences && SettingsData.screenPreferences[componentId] || ["all"];
}
function setScreenPreferences(componentId, screenNames) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
SettingsData.updateBarConfig(barId, {
screenPreferences: screenNames
});
return;
}
var prefs = SettingsData.screenPreferences || {};
var newPrefs = Object.assign({}, prefs);
newPrefs[componentId] = screenNames;
SettingsData.set("screenPreferences", newPrefs);
}
function getShowOnLastDisplay(componentId) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
const barConfig = SettingsData.getBarConfig(barId);
return barConfig?.showOnLastDisplay ?? true;
}
return SettingsData.showOnLastDisplay && SettingsData.showOnLastDisplay[componentId] || false;
}
function setShowOnLastDisplay(componentId, enabled) {
if (componentId.startsWith("bar:")) {
const barId = componentId.substring(4);
SettingsData.updateBarConfig(barId, {
showOnLastDisplay: enabled
});
return;
}
var prefs = SettingsData.showOnLastDisplay || {};
var newPrefs = Object.assign({}, prefs);
newPrefs[componentId] = enabled;
SettingsData.set("showOnLastDisplay", newPrefs);
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
@@ -181,16 +74,17 @@ Item {
width: parent.width width: parent.width
text: I18n.tr("Night Mode") text: I18n.tr("Night Mode")
description: DisplayService.gammaControlAvailable ? I18n.tr("Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates.") : I18n.tr("Gamma control not available. Requires DMS API v6+.") description: DisplayService.gammaControlAvailable ? I18n.tr("Apply warm color temperature to reduce eye strain. Use automation settings below to control when it activates.") : I18n.tr(
"Gamma control not available. Requires DMS API v6+.")
checked: DisplayService.nightModeEnabled checked: DisplayService.nightModeEnabled
enabled: DisplayService.gammaControlAvailable enabled: DisplayService.gammaControlAvailable
onToggled: checked => { onToggled: checked => {
DisplayService.toggleNightMode(); DisplayService.toggleNightMode()
} }
Connections { Connections {
function onNightModeEnabledChanged() { function onNightModeEnabledChanged() {
nightModeToggle.checked = DisplayService.nightModeEnabled; nightModeToggle.checked = DisplayService.nightModeEnabled
} }
target: DisplayService target: DisplayService
@@ -210,17 +104,17 @@ Item {
description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply") description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply")
currentValue: SessionData.nightModeTemperature + "K" currentValue: SessionData.nightModeTemperature + "K"
options: { options: {
var temps = []; var temps = []
for (var i = 2500; i <= 6000; i += 500) { for (var i = 2500; i <= 6000; i += 500) {
temps.push(i + "K"); temps.push(i + "K")
} }
return temps; return temps
} }
onValueChanged: value => { onValueChanged: value => {
var temp = parseInt(value.replace("K", "")); var temp = parseInt(value.replace("K", ""))
SessionData.setNightModeTemperature(temp); SessionData.setNightModeTemperature(temp)
if (SessionData.nightModeHighTemperature < temp) { if (SessionData.nightModeHighTemperature < temp) {
SessionData.setNightModeHighTemperature(temp); SessionData.setNightModeHighTemperature(temp)
} }
} }
} }
@@ -232,17 +126,17 @@ Item {
currentValue: SessionData.nightModeHighTemperature + "K" currentValue: SessionData.nightModeHighTemperature + "K"
visible: SessionData.nightModeAutoEnabled visible: SessionData.nightModeAutoEnabled
options: { options: {
var temps = []; var temps = []
var minTemp = SessionData.nightModeTemperature; var minTemp = SessionData.nightModeTemperature
for (var i = Math.max(2500, minTemp); i <= 10000; i += 500) { for (var i = Math.max(2500, minTemp); i <= 10000; i += 500) {
temps.push(i + "K"); temps.push(i + "K")
} }
return temps; return temps
} }
onValueChanged: value => { onValueChanged: value => {
var temp = parseInt(value.replace("K", "")); var temp = parseInt(value.replace("K", ""))
if (temp >= SessionData.nightModeTemperature) { if (temp >= SessionData.nightModeTemperature) {
SessionData.setNightModeHighTemperature(temp); SessionData.setNightModeHighTemperature(temp)
} }
} }
} }
@@ -257,17 +151,17 @@ Item {
visible: DisplayService.gammaControlAvailable visible: DisplayService.gammaControlAvailable
onToggled: checked => { onToggled: checked => {
if (checked && !DisplayService.nightModeEnabled) { if (checked && !DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode(); DisplayService.toggleNightMode()
} else if (!checked && DisplayService.nightModeEnabled) { } else if (!checked && DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode(); DisplayService.toggleNightMode()
} }
SessionData.setNightModeAutoEnabled(checked); SessionData.setNightModeAutoEnabled(checked)
} }
Connections { Connections {
target: SessionData target: SessionData
function onNightModeAutoEnabledChanged() { function onNightModeAutoEnabledChanged() {
automaticToggle.checked = SessionData.nightModeAutoEnabled; automaticToggle.checked = SessionData.nightModeAutoEnabled
} }
} }
} }
@@ -281,7 +175,7 @@ Item {
Connections { Connections {
target: SessionData target: SessionData
function onNightModeAutoEnabledChanged() { function onNightModeAutoEnabledChanged() {
automaticSettings.visible = SessionData.nightModeAutoEnabled; automaticSettings.visible = SessionData.nightModeAutoEnabled
} }
} }
@@ -294,32 +188,29 @@ Item {
width: 200 width: 200
height: 45 height: 45
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
model: [ model: [{
{
"text": "Time", "text": "Time",
"icon": "access_time" "icon": "access_time"
}, }, {
{
"text": "Location", "text": "Location",
"icon": "place" "icon": "place"
} }]
]
Component.onCompleted: { Component.onCompleted: {
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(updateIndicator); Qt.callLater(updateIndicator)
} }
onTabClicked: index => { onTabClicked: index => {
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time"); DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
currentIndex = index; currentIndex = index
} }
Connections { Connections {
target: SessionData target: SessionData
function onNightModeAutoModeChanged() { function onNightModeAutoModeChanged() {
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0; modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(modeTabBarNight.updateIndicator); Qt.callLater(modeTabBarNight.updateIndicator)
} }
} }
} }
@@ -376,14 +267,14 @@ Item {
dropdownWidth: 70 dropdownWidth: 70
currentValue: SessionData.nightModeStartHour.toString() currentValue: SessionData.nightModeStartHour.toString()
options: { options: {
var hours = []; var hours = []
for (var i = 0; i < 24; i++) { for (var i = 0; i < 24; i++) {
hours.push(i.toString()); hours.push(i.toString())
} }
return hours; return hours
} }
onValueChanged: value => { onValueChanged: value => {
SessionData.setNightModeStartHour(parseInt(value)); SessionData.setNightModeStartHour(parseInt(value))
} }
} }
@@ -391,14 +282,14 @@ Item {
dropdownWidth: 70 dropdownWidth: 70
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0') currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
options: { options: {
var minutes = []; var minutes = []
for (var i = 0; i < 60; i += 5) { for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0')); minutes.push(i.toString().padStart(2, '0'))
} }
return minutes; return minutes
} }
onValueChanged: value => { onValueChanged: value => {
SessionData.setNightModeStartMinute(parseInt(value)); SessionData.setNightModeStartMinute(parseInt(value))
} }
} }
} }
@@ -419,14 +310,14 @@ Item {
dropdownWidth: 70 dropdownWidth: 70
currentValue: SessionData.nightModeEndHour.toString() currentValue: SessionData.nightModeEndHour.toString()
options: { options: {
var hours = []; var hours = []
for (var i = 0; i < 24; i++) { for (var i = 0; i < 24; i++) {
hours.push(i.toString()); hours.push(i.toString())
} }
return hours; return hours
} }
onValueChanged: value => { onValueChanged: value => {
SessionData.setNightModeEndHour(parseInt(value)); SessionData.setNightModeEndHour(parseInt(value))
} }
} }
@@ -434,14 +325,14 @@ Item {
dropdownWidth: 70 dropdownWidth: 70
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0') currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
options: { options: {
var minutes = []; var minutes = []
for (var i = 0; i < 60; i += 5) { for (var i = 0; i < 60; i += 5) {
minutes.push(i.toString().padStart(2, '0')); minutes.push(i.toString().padStart(2, '0'))
} }
return minutes; return minutes
} }
onValueChanged: value => { onValueChanged: value => {
SessionData.setNightModeEndMinute(parseInt(value)); SessionData.setNightModeEndMinute(parseInt(value))
} }
} }
} }
@@ -461,13 +352,13 @@ Item {
description: I18n.tr("Automatically detect location based on IP address") description: I18n.tr("Automatically detect location based on IP address")
checked: SessionData.nightModeUseIPLocation || false checked: SessionData.nightModeUseIPLocation || false
onToggled: checked => { onToggled: checked => {
SessionData.setNightModeUseIPLocation(checked); SessionData.setNightModeUseIPLocation(checked)
} }
Connections { Connections {
target: SessionData target: SessionData
function onNightModeUseIPLocationChanged() { function onNightModeUseIPLocationChanged() {
ipLocationToggle.checked = SessionData.nightModeUseIPLocation; ipLocationToggle.checked = SessionData.nightModeUseIPLocation
} }
} }
} }
@@ -502,9 +393,9 @@ Item {
text: SessionData.latitude.toString() text: SessionData.latitude.toString()
placeholderText: "0.0" placeholderText: "0.0"
onEditingFinished: { onEditingFinished: {
const lat = parseFloat(text); const lat = parseFloat(text)
if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) { if (!isNaN(lat) && lat >= -90 && lat <= 90 && lat !== SessionData.latitude) {
SessionData.setLatitude(lat); SessionData.setLatitude(lat)
} }
} }
} }
@@ -525,9 +416,9 @@ Item {
text: SessionData.longitude.toString() text: SessionData.longitude.toString()
placeholderText: "0.0" placeholderText: "0.0"
onEditingFinished: { onEditingFinished: {
const lon = parseFloat(text); const lon = parseFloat(text)
if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) { if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) {
SessionData.setLongitude(lon); SessionData.setLongitude(lon)
} }
} }
} }
@@ -678,7 +569,7 @@ Item {
} }
StyledText { StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunriseTime) text: root.formatGammaTime(DisplayService.gammaSunriseTime)
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -714,7 +605,7 @@ Item {
} }
StyledText { StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunsetTime) text: root.formatGammaTime(DisplayService.gammaSunsetTime)
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -761,7 +652,7 @@ Item {
} }
StyledText { StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaNextTransition) text: root.formatGammaTime(DisplayService.gammaNextTransition)
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -773,371 +664,6 @@ Item {
} }
} }
} }
StyledRect {
width: parent.width
height: screensInfoSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: screensInfoSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Connected Displays")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Configure which displays show shell components")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Available Screens (") + Quickshell.screens.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Item {
width: 1
height: 1
Layout.fillWidth: true
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: I18n.tr("Display Name Format")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
DankButtonGroup {
id: displayModeGroup
model: [I18n.tr("Name"), I18n.tr("Model")]
currentIndex: SettingsData.displayNameMode === "model" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.displayNameMode = index === 1 ? "model" : "system";
SettingsData.saveSettings();
}
Connections {
target: SettingsData
function onDisplayNameModeChanged() {
displayModeGroup.currentIndex = SettingsData.displayNameMode === "model" ? 1 : 0;
}
}
}
}
}
}
Repeater {
model: Quickshell.screens
delegate: Rectangle {
width: parent.width
height: screenRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 0
Row {
id: screenRow
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingM
DankIcon {
name: "desktop_windows"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS / 2
StyledText {
text: SettingsData.getScreenDisplayName(modelData)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Row {
spacing: Theme.spacingS
property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null
property var currentMode: wlrOutput?.currentMode
StyledText {
text: {
if (parent.currentMode) {
return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz";
}
return modelData.width + "×" + modelData.height;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: SettingsData.displayNameMode === "system" ? (modelData.model || "Unknown Model") : modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingL
Repeater {
model: displaysTab.variantComponents
delegate: StyledRect {
width: parent.width
height: componentSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
Column {
id: componentSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Show on screens:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
property string componentId: modelData.id
width: parent.width
spacing: Theme.spacingXS
DankToggle {
width: parent.width
text: I18n.tr("All displays")
description: I18n.tr("Show on all connected displays")
checked: {
var prefs = displaysTab.getScreenPreferences(parent.componentId);
return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
}
onToggled: checked => {
if (checked) {
displaysTab.setScreenPreferences(parent.componentId, ["all"]);
} else {
displaysTab.setScreenPreferences(parent.componentId, []);
const cid = parent.componentId;
if (["dankBar", "dock", "notifications", "osd", "toast"].includes(cid) || cid.startsWith("bar:")) {
displaysTab.setShowOnLastDisplay(cid, true);
}
}
}
}
DankToggle {
width: parent.width
text: I18n.tr("Show on Last Display")
description: I18n.tr("Always show when there's only one connected display")
checked: displaysTab.getShowOnLastDisplay(parent.componentId)
visible: {
const prefs = displaysTab.getScreenPreferences(parent.componentId);
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
const cid = parent.componentId;
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:");
return !isAll && isRelevantComponent;
}
onToggled: checked => {
displaysTab.setShowOnLastDisplay(parent.componentId, checked);
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.2
visible: {
var prefs = displaysTab.getScreenPreferences(parent.componentId);
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: {
var prefs = displaysTab.getScreenPreferences(parent.componentId);
return !prefs.includes("all") && !(typeof prefs[0] === "string" && prefs[0] === "all");
}
Repeater {
model: Quickshell.screens
delegate: DankToggle {
property var screenData: modelData
property string componentId: parent.parent.componentId
width: parent.width
text: SettingsData.getScreenDisplayName(screenData)
description: screenData.width + "×" + screenData.height + " • " + (SettingsData.displayNameMode === "system" ? (screenData.model || "Unknown Model") : screenData.name)
checked: {
var prefs = displaysTab.getScreenPreferences(componentId);
if (typeof prefs[0] === "string" && prefs[0] === "all")
return false;
return SettingsData.isScreenInPreferences(screenData, prefs);
}
onToggled: checked => {
var currentPrefs = displaysTab.getScreenPreferences(componentId);
if (typeof currentPrefs[0] === "string" && currentPrefs[0] === "all") {
currentPrefs = [];
}
const screenModelIndex = SettingsData.getScreenModelIndex(screenData);
var newPrefs = currentPrefs.filter(pref => {
if (typeof pref === "string")
return false;
if (pref.modelIndex !== undefined && screenModelIndex >= 0) {
return !(pref.model === screenData.model && pref.modelIndex === screenModelIndex);
}
return pref.name !== screenData.name || pref.model !== screenData.model;
});
if (checked) {
const prefObj = {
name: screenData.name,
model: screenData.model || ""
};
if (screenModelIndex >= 0) {
prefObj.modelIndex = screenModelIndex;
}
newPrefs.push(prefObj);
}
displaysTab.setScreenPreferences(componentId, newPrefs);
}
}
}
}
}
}
}
}
}
}
} }
} }
} }
+16 -29
View File
@@ -229,7 +229,7 @@ Item {
DankTextField { DankTextField {
id: searchField id: searchField
width: parent.width - addButton.width - Theme.spacingM width: parent.width - addButton.width - Theme.spacingM
height: 44 height: Math.round(Theme.fontSizeMedium * 3)
placeholderText: I18n.tr("Search keybinds...") placeholderText: I18n.tr("Search keybinds...")
leftIconName: "search" leftIconName: "search"
onTextChanged: { onTextChanged: {
@@ -240,8 +240,8 @@ Item {
DankActionButton { DankActionButton {
id: addButton id: addButton
width: 44 width: searchField.height
height: 44 height: searchField.height
circular: false circular: false
iconName: "add" iconName: "add"
iconSize: Theme.iconSize iconSize: Theme.iconSize
@@ -328,40 +328,25 @@ Item {
} }
} }
Rectangle { DankButton {
id: fixButton id: fixButton
width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: 36
radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: fixButtonText
text: { text: {
if (KeybindsService.fixing) if (KeybindsService.fixing)
return I18n.tr("Fixing..."); return I18n.tr("Fixing...")
if (warningBox.showSetup) if (warningBox.showSetup)
return I18n.tr("Setup"); return I18n.tr("Setup")
return I18n.tr("Fix Now"); return I18n.tr("Fix Now")
} }
font.pixelSize: Theme.fontSizeSmall backgroundColor: Theme.primary
font.weight: Font.Medium textColor: Theme.primaryText
color: Theme.surface
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: !KeybindsService.fixing enabled: !KeybindsService.fixing
anchors.verticalCenter: parent.verticalCenter
onClicked: KeybindsService.fixDmsBindsInclude() onClicked: KeybindsService.fixDmsBindsInclude()
} }
} }
} }
} }
}
StyledRect { StyledRect {
width: Math.min(650, parent.width - Theme.spacingL * 2) width: Math.min(650, parent.width - Theme.spacingL * 2)
@@ -382,9 +367,10 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
Rectangle { Rectangle {
readonly property real chipHeight: allChip.implicitHeight + Theme.spacingM
width: allChip.implicitWidth + Theme.spacingL width: allChip.implicitWidth + Theme.spacingL
height: 32 height: chipHeight
radius: 16 radius: chipHeight / 2
color: !keybindsTab.selectedCategory ? Theme.primary : Theme.surfaceContainerHighest color: !keybindsTab.selectedCategory ? Theme.primary : Theme.surfaceContainerHighest
StyledText { StyledText {
@@ -412,9 +398,10 @@ Item {
required property string modelData required property string modelData
required property int index required property int index
readonly property real chipHeight: catText.implicitHeight + Theme.spacingM
width: catText.implicitWidth + Theme.spacingL width: catText.implicitWidth + Theme.spacingL
height: 32 height: chipHeight
radius: 16 radius: chipHeight / 2
color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceContainerHighest) color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceContainerHighest)
StyledText { StyledText {
@@ -23,6 +23,15 @@ Item {
iconName: "refresh" iconName: "refresh"
title: I18n.tr("System Updater") title: I18n.tr("System Updater")
SettingsToggleRow {
text: I18n.tr("Hide Updater Widget", "When updater widget is used, then hide it if no update found")
description: I18n.tr("When updater widget is used, then hide it if no update found")
checked: SettingsData.updaterHideWidget
onToggled: checked => {
SettingsData.set("updaterHideWidget", checked);
}
}
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Use Custom Command") text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system") description: I18n.tr("Use custom command for update your system")
+93 -46
View File
@@ -371,9 +371,14 @@ Item {
widgetObj.pciId = ""; widgetObj.pciId = "";
} }
if (widgetId === "controlCenterButton") { if (widgetId === "controlCenterButton") {
widgetObj.showNetworkIcon = true; widgetObj.showNetworkIcon = SettingsData.controlCenterShowNetworkIcon;
widgetObj.showBluetoothIcon = true; widgetObj.showBluetoothIcon = SettingsData.controlCenterShowBluetoothIcon;
widgetObj.showAudioIcon = true; widgetObj.showAudioIcon = SettingsData.controlCenterShowAudioIcon;
widgetObj.showVpnIcon = SettingsData.controlCenterShowVpnIcon;
widgetObj.showBrightnessIcon = SettingsData.controlCenterShowBrightnessIcon;
widgetObj.showMicIcon = SettingsData.controlCenterShowMicIcon;
widgetObj.showBatteryIcon = SettingsData.controlCenterShowBatteryIcon;
widgetObj.showPrinterIcon = SettingsData.controlCenterShowPrinterIcon;
} }
if (widgetId === "diskUsage") if (widgetId === "diskUsage")
widgetObj.mountPath = "/"; widgetObj.mountPath = "/";
@@ -423,9 +428,14 @@ Item {
else if (widget.id === "gpuTemp") else if (widget.id === "gpuTemp")
newWidget.pciId = ""; newWidget.pciId = "";
if (widget.id === "controlCenterButton") { if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true; newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true; newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? true; newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
} }
widgets[i] = newWidget; widgets[i] = newWidget;
break; break;
@@ -471,9 +481,14 @@ Item {
if (widget.pciId !== undefined) if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId; newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") { if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true; newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true; newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? true; newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
} }
widgets[widgetIndex] = newWidget; widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
@@ -541,41 +556,48 @@ Item {
if (widget.pciId !== undefined) if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId; newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") { if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true; newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true; newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? true; newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
} }
widgets[widgetIndex] = newWidget; widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
} }
function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) { function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) {
switch (settingName) { var widgets = getWidgetsForSection(sectionId).slice();
case "showNetworkIcon": if (widgetIndex < 0 || widgetIndex >= widgets.length)
SettingsData.set("controlCenterShowNetworkIcon", value); return;
break;
case "showBluetoothIcon": var widget = widgets[widgetIndex];
SettingsData.set("controlCenterShowBluetoothIcon", value); if (typeof widget === "string") {
break; widget = {
case "showAudioIcon": "id": widget,
SettingsData.set("controlCenterShowAudioIcon", value); "enabled": true
break; };
case "showVpnIcon":
SettingsData.set("controlCenterShowVpnIcon", value);
break;
case "showBrightnessIcon":
SettingsData.set("controlCenterShowBrightnessIcon", value);
break;
case "showMicIcon":
SettingsData.set("controlCenterShowMicIcon", value);
break;
case "showBatteryIcon":
SettingsData.set("controlCenterShowBatteryIcon", value);
break;
case "showPrinterIcon":
SettingsData.set("controlCenterShowPrinterIcon", value);
break;
} }
var newWidget = {
"id": widget.id,
"enabled": widget.enabled !== undefined ? widget.enabled : true,
"showNetworkIcon": widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon,
"showBluetoothIcon": widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon,
"showAudioIcon": widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon,
"showVpnIcon": widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon,
"showBrightnessIcon": widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon,
"showMicIcon": widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon,
"showBatteryIcon": widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon,
"showPrinterIcon": widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon
};
newWidget[settingName] = value;
widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets);
} }
function handlePrivacySettingChanged(sectionId, widgetIndex, settingName, value) { function handlePrivacySettingChanged(sectionId, widgetIndex, settingName, value) {
@@ -626,9 +648,14 @@ Item {
if (widget.showSwap !== undefined) if (widget.showSwap !== undefined)
newWidget.showSwap = widget.showSwap; newWidget.showSwap = widget.showSwap;
if (widget.id === "controlCenterButton") { if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true; newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true; newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? true; newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
} }
widgets[widgetIndex] = newWidget; widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
@@ -678,9 +705,14 @@ Item {
if (widget.keyboardLayoutNameCompactMode !== undefined) if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode; newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") { if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true; newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true; newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? true; newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
} }
widgets[widgetIndex] = newWidget; widgets[widgetIndex] = newWidget;
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
@@ -730,9 +762,14 @@ Item {
if (widget.keyboardLayoutNameCompactMode !== undefined) if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode; newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") { if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true; newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true; newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
newWidget.showAudioIcon = widget.showAudioIcon ?? true; newWidget.showAudioIcon = widget.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
newWidget.showVpnIcon = widget.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
newWidget.showBrightnessIcon = widget.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
newWidget.showMicIcon = widget.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
newWidget.showBatteryIcon = widget.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
newWidget.showPrinterIcon = widget.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
} }
widgets[i] = newWidget; widgets[i] = newWidget;
widget = newWidget; widget = newWidget;
@@ -789,6 +826,16 @@ Item {
item.showBluetoothIcon = widget.showBluetoothIcon; item.showBluetoothIcon = widget.showBluetoothIcon;
if (widget.showAudioIcon !== undefined) if (widget.showAudioIcon !== undefined)
item.showAudioIcon = widget.showAudioIcon; item.showAudioIcon = widget.showAudioIcon;
if (widget.showVpnIcon !== undefined)
item.showVpnIcon = widget.showVpnIcon;
if (widget.showBrightnessIcon !== undefined)
item.showBrightnessIcon = widget.showBrightnessIcon;
if (widget.showMicIcon !== undefined)
item.showMicIcon = widget.showMicIcon;
if (widget.showBatteryIcon !== undefined)
item.showBatteryIcon = widget.showBatteryIcon;
if (widget.showPrinterIcon !== undefined)
item.showPrinterIcon = widget.showPrinterIcon;
if (widget.minimumWidth !== undefined) if (widget.minimumWidth !== undefined)
item.minimumWidth = widget.minimumWidth; item.minimumWidth = widget.minimumWidth;
if (widget.showSwap !== undefined) if (widget.showSwap !== undefined)
@@ -806,50 +806,42 @@ Column {
{ {
icon: "lan", icon: "lan",
label: I18n.tr("Network"), label: I18n.tr("Network"),
setting: "showNetworkIcon", setting: "showNetworkIcon"
checked: SettingsData.controlCenterShowNetworkIcon
}, },
{ {
icon: "vpn_lock", icon: "vpn_lock",
label: I18n.tr("VPN"), label: I18n.tr("VPN"),
setting: "showVpnIcon", setting: "showVpnIcon"
checked: SettingsData.controlCenterShowVpnIcon
}, },
{ {
icon: "bluetooth", icon: "bluetooth",
label: I18n.tr("Bluetooth"), label: I18n.tr("Bluetooth"),
setting: "showBluetoothIcon", setting: "showBluetoothIcon"
checked: SettingsData.controlCenterShowBluetoothIcon
}, },
{ {
icon: "volume_up", icon: "volume_up",
label: I18n.tr("Audio"), label: I18n.tr("Audio"),
setting: "showAudioIcon", setting: "showAudioIcon"
checked: SettingsData.controlCenterShowAudioIcon
}, },
{ {
icon: "mic", icon: "mic",
label: I18n.tr("Microphone"), label: I18n.tr("Microphone"),
setting: "showMicIcon", setting: "showMicIcon"
checked: SettingsData.controlCenterShowMicIcon
}, },
{ {
icon: "brightness_high", icon: "brightness_high",
label: I18n.tr("Brightness"), label: I18n.tr("Brightness"),
setting: "showBrightnessIcon", setting: "showBrightnessIcon"
checked: SettingsData.controlCenterShowBrightnessIcon
}, },
{ {
icon: "battery_full", icon: "battery_full",
label: I18n.tr("Battery"), label: I18n.tr("Battery"),
setting: "showBatteryIcon", setting: "showBatteryIcon"
checked: SettingsData.controlCenterShowBatteryIcon
}, },
{ {
icon: "print", icon: "print",
label: I18n.tr("Printer"), label: I18n.tr("Printer"),
setting: "showPrinterIcon", setting: "showPrinterIcon"
checked: SettingsData.controlCenterShowPrinterIcon
} }
] ]
@@ -857,6 +849,30 @@ Column {
required property var modelData required property var modelData
required property int index required property int index
function getCheckedState() {
var wd = controlCenterContextMenu.widgetData;
switch (modelData.setting) {
case "showNetworkIcon":
return wd?.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
case "showVpnIcon":
return wd?.showVpnIcon ?? SettingsData.controlCenterShowVpnIcon;
case "showBluetoothIcon":
return wd?.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
case "showAudioIcon":
return wd?.showAudioIcon ?? SettingsData.controlCenterShowAudioIcon;
case "showMicIcon":
return wd?.showMicIcon ?? SettingsData.controlCenterShowMicIcon;
case "showBrightnessIcon":
return wd?.showBrightnessIcon ?? SettingsData.controlCenterShowBrightnessIcon;
case "showBatteryIcon":
return wd?.showBatteryIcon ?? SettingsData.controlCenterShowBatteryIcon;
case "showPrinterIcon":
return wd?.showPrinterIcon ?? SettingsData.controlCenterShowPrinterIcon;
default:
return false;
}
}
width: menuColumn.width width: menuColumn.width
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -891,7 +907,7 @@ Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 40 width: 40
height: 20 height: 20
checked: modelData.checked checked: getCheckedState()
onToggled: { onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled); root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled);
} }
+3 -2
View File
@@ -216,8 +216,9 @@ Variants {
} }
readonly property int maxTextureSize: 8192 readonly property int maxTextureSize: 8192
property int textureWidth: Math.min(modelData.width, maxTextureSize) property real screenScale: CompositorService.getScreenScale(modelData)
property int textureHeight: Math.min(modelData.height, maxTextureSize) property int textureWidth: Math.min(Math.round(modelData.width * screenScale), maxTextureSize)
property int textureHeight: Math.min(Math.round(modelData.height * screenScale), maxTextureSize)
Image { Image {
id: currentWallpaper id: currentWallpaper
+1 -1
View File
@@ -30,7 +30,7 @@ Singleton {
id: cavaProcess id: cavaProcess
running: root.cavaAvailable && root.refCount > 0 running: root.cavaAvailable && root.refCount > 0
command: ["sh", "-c", "printf '[general]\\nframerate=25\\nbars=6\\nautosens=0\\nsensitivity=30\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[input]\\nmethod=pipewire\\nsource=auto\\nsample_rate=48000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin"] command: ["sh", "-c", "printf '[general]\\nframerate=25\\nbars=6\\nautosens=0\\nsensitivity=30\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[input]\\nsample_rate=48000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin"]
onRunningChanged: { onRunningChanged: {
if (!running) { if (!running) {
+8 -1
View File
@@ -53,10 +53,13 @@ Singleton {
signal gammaStateUpdate(var data) signal gammaStateUpdate(var data)
signal openUrlRequested(string url) signal openUrlRequested(string url)
signal appPickerRequested(var data) signal appPickerRequested(var data)
signal screensaverStateUpdate(var data)
property bool capsLockState: false property bool capsLockState: false
property bool screensaverInhibited: false
property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser"] property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser"]
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -371,6 +374,10 @@ Singleton {
} else if (data.url) { } else if (data.url) {
openUrlRequested(data.url); openUrlRequested(data.url);
} }
} else if (service === "freedesktop.screensaver") {
screensaverInhibited = data.inhibited || false;
screensaverInhibitors = data.inhibitors || [];
screensaverStateUpdate(data);
} }
} }
+87
View File
@@ -1,13 +1,19 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common
Singleton { Singleton {
id: root id: root
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string mangoDmsDir: configDir + "/mango/dms"
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
property bool dwlAvailable: false property bool dwlAvailable: false
property var outputs: ({}) property var outputs: ({})
property var tagCount: 9 property var tagCount: 9
@@ -263,4 +269,85 @@ Singleton {
return Array.from(visibleTags).sort((a, b) => a - b); return Array.from(visibleTags).sort((a, b) => a - b);
} }
function generateOutputsConfig(outputsData) {
if (!outputsData || Object.keys(outputsData).length === 0)
return
let lines = ["# Auto-generated by DMS - do not edit manually", "# VRR is global: set adaptive_sync=1 in config.conf", ""]
for (const outputName in outputsData) {
const output = outputsData[outputName]
if (!output)
continue
let width = 1920
let height = 1080
let refreshRate = 60
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode]
if (mode) {
width = mode.width || 1920
height = mode.height || 1080
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000)
}
}
const x = output.logical?.x ?? 0
const y = output.logical?.y ?? 0
const scale = output.logical?.scale ?? 1.0
const transform = transformToMango(output.logical?.transform ?? "Normal")
const rule = [
outputName,
"0.55",
"1",
"tile",
transform,
scale,
x,
y,
width,
height,
refreshRate
].join(",")
lines.push("monitorrule=" + rule)
}
lines.push("")
const content = lines.join("\n")
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("DwlService: Failed to write outputs config:", output)
return
}
console.info("DwlService: Generated outputs config at", outputsPath)
if (CompositorService.isDwl)
reloadConfig()
})
}
function reloadConfig() {
Proc.runCommand("mango-reload", ["mmsg", "-d", "reload_config"], (output, exitCode) => {
if (exitCode !== 0)
console.warn("DwlService: mmsg reload_config failed:", output)
})
}
function transformToMango(transform) {
switch (transform) {
case "Normal": return 0
case "90": return 1
case "180": return 2
case "270": return 3
case "Flipped": return 4
case "Flipped90": return 5
case "Flipped180": return 6
case "Flipped270": return 7
default: return 0
}
}
} }
+110
View File
@@ -0,0 +1,110 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Common
Singleton {
id: root
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string hyprDmsDir: configDir + "/hypr/dms"
readonly property string outputsPath: hyprDmsDir + "/outputs.conf"
function getOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
return "desc:" + output.make + " " + output.model
}
return outputName
}
function generateOutputsConfig(outputsData) {
if (!outputsData || Object.keys(outputsData).length === 0)
return
let lines = ["# Auto-generated by DMS - do not edit manually", ""]
for (const outputName in outputsData) {
const output = outputsData[outputName]
if (!output)
continue
let resolution = "preferred"
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode]
if (mode)
resolution = mode.width + "x" + mode.height + "@" + (mode.refresh_rate / 1000).toFixed(3)
}
const x = output.logical?.x ?? 0
const y = output.logical?.y ?? 0
const position = x + "x" + y
const scale = output.logical?.scale ?? 1.0
const identifier = getOutputIdentifier(output, outputName)
let monitorLine = "monitor = " + identifier + ", " + resolution + ", " + position + ", " + scale
const transform = transformToHyprland(output.logical?.transform ?? "Normal")
if (transform !== 0)
monitorLine += ", transform, " + transform
if (output.vrr_supported && output.vrr_enabled)
monitorLine += ", vrr, 1"
lines.push(monitorLine)
}
lines.push("")
const content = lines.join("\n")
Proc.runCommand("hypr-write-outputs", ["sh", "-c", `mkdir -p "${hyprDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("HyprlandService: Failed to write outputs config:", output)
return
}
console.info("HyprlandService: Generated outputs config at", outputsPath)
if (CompositorService.isHyprland)
reloadConfig()
})
}
function reloadConfig() {
Proc.runCommand("hyprctl-reload", ["hyprctl", "reload"], (output, exitCode) => {
if (exitCode !== 0)
console.warn("HyprlandService: hyprctl reload failed:", output)
})
}
function transformToHyprland(transform) {
switch (transform) {
case "Normal": return 0
case "90": return 1
case "180": return 2
case "270": return 3
case "Flipped": return 4
case "Flipped90": return 5
case "Flipped180": return 6
case "Flipped270": return 7
default: return 0
}
}
function hyprlandToTransform(value) {
switch (value) {
case 0: return "Normal"
case 1: return "90"
case 2: return "180"
case 3: return "270"
case 4: return "Flipped"
case 5: return "Flipped90"
case 6: return "Flipped180"
case 7: return "Flipped270"
default: return "Normal"
}
}
}
+21
View File
@@ -29,6 +29,8 @@ Singleton {
property bool respectInhibitors: true property bool respectInhibitors: true
property bool _enableGate: true property bool _enableGate: true
readonly property bool externalInhibitActive: DMSService.screensaverInhibited
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout readonly property int lockTimeout: isOnBattery ? SettingsData.batteryLockTimeout : SettingsData.acLockTimeout
@@ -141,6 +143,19 @@ Singleton {
} }
} }
onExternalInhibitActiveChanged: {
if (externalInhibitActive) {
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
console.info("IdleService: External idle inhibit active from:", apps || "unknown");
SessionService.idleInhibited = true;
SessionService.inhibitReason = "External app: " + (apps || "unknown");
} else {
console.info("IdleService: External idle inhibit released");
SessionService.idleInhibited = false;
SessionService.inhibitReason = "Keep system awake";
}
}
Component.onCompleted: { Component.onCompleted: {
if (!idleMonitorAvailable) { if (!idleMonitorAvailable) {
console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell."); console.warn("IdleService: IdleMonitor not available - power management disabled. This requires a newer version of Quickshell.");
@@ -148,5 +163,11 @@ Singleton {
console.info("IdleService: Initialized with idle monitoring support"); console.info("IdleService: Initialized with idle monitoring support");
createIdleMonitors(); createIdleMonitors();
} }
if (externalInhibitActive) {
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
SessionService.idleInhibited = true;
SessionService.inhibitReason = "External app: " + (apps || "unknown");
}
} }
} }
+197 -20
View File
@@ -1,5 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtCore import QtCore
import QtQuick import QtQuick
@@ -23,6 +23,8 @@ Singleton {
property var windows: [] property var windows: []
property var displayScales: ({}) property var displayScales: ({})
property var _realOutputs: ({})
property bool inOverview: false property bool inOverview: false
property int currentKeyboardLayoutIndex: 0 property int currentKeyboardLayoutIndex: 0
@@ -66,6 +68,19 @@ Singleton {
onTriggered: root.doGenerateNiriLayoutConfig() onTriggered: root.doGenerateNiriLayoutConfig()
} }
property int _lastGapValue: -1
Connections {
target: SettingsData
function onBarConfigsChanged() {
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
if (newGaps === root._lastGapValue)
return;
root._lastGapValue = newGaps;
generateNiriLayoutConfig();
}
}
Process { Process {
id: validateProcess id: validateProcess
command: ["niri", "validate"] command: ["niri", "validate"]
@@ -201,12 +216,12 @@ Singleton {
const ws = workspaces[w.workspace_id]; const ws = workspaces[w.workspace_id];
if (!ws) { if (!ws) {
return { return {
window: w, "window": w,
outputX: 999999, "outputX": 999999,
outputY: 999999, "outputY": 999999,
wsIdx: 999999, "wsIdx": 999999,
col: 999999, "col": 999999,
row: 999999 "row": 999999
}; };
} }
@@ -219,12 +234,12 @@ Singleton {
const row = (pos && pos.length >= 2) ? pos[1] : 999999; const row = (pos && pos.length >= 2) ? pos[1] : 999999;
return { return {
window: w, "window": w,
outputX: outputX, "outputX": outputX,
outputY: outputY, "outputY": outputY,
wsIdx: ws.idx, "wsIdx": ws.idx,
col: col, "col": col,
row: row "row": row
}; };
}); });
@@ -291,6 +306,9 @@ Singleton {
case 'WorkspaceUrgencyChanged': case 'WorkspaceUrgencyChanged':
handleWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged); handleWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged);
break; break;
case 'WindowUrgencyChanged':
handleWindowUrgencyChanged(event.WindowUrgencyChanged);
break;
case 'ScreenshotCaptured': case 'ScreenshotCaptured':
handleScreenshotCaptured(event.ScreenshotCaptured); handleScreenshotCaptured(event.ScreenshotCaptured);
break; break;
@@ -558,6 +576,23 @@ Singleton {
windowUrgentChanged(); windowUrgentChanged();
} }
function handleWindowUrgencyChanged(data) {
const windowIndex = windows.findIndex(w => w.id === data.id);
if (windowIndex < 0)
return;
const updatedWindows = [...windows];
const updatedWindow = {};
for (let prop in updatedWindows[windowIndex]) {
updatedWindow[prop] = updatedWindows[windowIndex][prop];
}
updatedWindow.is_urgent = data.urgent;
updatedWindows[windowIndex] = updatedWindow;
windows = updatedWindows;
windowUrgentChanged();
}
function handleScreenshotCaptured(data) { function handleScreenshotCaptured(data) {
if (!data.path) if (!data.path)
return; return;
@@ -565,7 +600,7 @@ Singleton {
const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR"); const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path]; const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path];
Quickshell.execDetached({ Quickshell.execDetached({
command: command "command": command
}); });
pendingScreenshotPath = ""; pendingScreenshotPath = "";
} }
@@ -959,7 +994,14 @@ Singleton {
const cornerRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12; const cornerRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4; const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const configContent = `layout { const dmsWarning = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`;
const configContent = dmsWarning + `layout {
gaps ${gaps} gaps ${gaps}
border { border {
@@ -969,19 +1011,19 @@ Singleton {
focus-ring { focus-ring {
width 2 width 2
} }
} }
window-rule { window-rule {
geometry-corner-radius ${cornerRadius} geometry-corner-radius ${cornerRadius}
clip-to-geometry true clip-to-geometry true
tiled-state true tiled-state true
draw-border-with-background false draw-border-with-background false
}`; }`;
const alttabContent = `recent-windows { const alttabContent = dmsWarning + `recent-windows {
highlight { highlight {
corner-radius ${cornerRadius} corner-radius ${cornerRadius}
} }
}`; }`;
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms"; const niriDmsDir = configDir + "/niri/dms";
@@ -1014,6 +1056,141 @@ window-rule {
writeBlurruleProcess.running = true; writeBlurruleProcess.running = true;
} }
function updateOutputPosition(outputName, x, y) {
if (!outputs || !outputs[outputName])
return;
const updatedOutputs = {};
for (const name in outputs) {
const output = outputs[name];
if (name === outputName && output.logical) {
updatedOutputs[name] = JSON.parse(JSON.stringify(output));
updatedOutputs[name].logical.x = x;
updatedOutputs[name].logical.y = y;
} else {
updatedOutputs[name] = output;
}
}
outputs = updatedOutputs;
}
function applyOutputConfig(outputName, config, callback) {
if (!CompositorService.isNiri || !outputName) {
if (callback)
callback(false, "Invalid config");
return;
}
const commands = [];
if (config.position !== undefined) {
commands.push(`niri msg output "${outputName}" position ${config.position.x} ${config.position.y}`);
}
if (config.mode !== undefined) {
commands.push(`niri msg output "${outputName}" mode ${config.mode}`);
}
if (config.vrr !== undefined) {
commands.push(`niri msg output "${outputName}" vrr ${config.vrr ? "on" : "off"}`);
}
if (config.scale !== undefined) {
commands.push(`niri msg output "${outputName}" scale ${config.scale}`);
}
if (config.transform !== undefined) {
commands.push(`niri msg output "${outputName}" transform "${config.transform}"`);
}
if (commands.length === 0) {
if (callback)
callback(true, "No changes");
return;
}
const fullCommand = commands.join(" && ");
Proc.runCommand("niri-output-config", ["sh", "-c", fullCommand], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("NiriService: Failed to apply output config:", output);
if (callback)
callback(false, output);
return;
}
console.info("NiriService: Applied output config for", outputName);
fetchOutputs();
if (callback)
callback(true, "Success");
});
}
function getOutputIdentifier(output, outputName) {
if (SettingsData.displayNameMode === "model" && output.make && output.model) {
const serial = output.serial || "Unknown";
return output.make + " " + output.model + " " + serial;
}
return outputName;
}
function generateOutputsConfig(outputsData) {
const data = outputsData || outputs;
if (!data || Object.keys(data).length === 0)
return;
let kdlContent = `// Auto-generated by DMS - do not edit manually\n\n`;
for (const outputName in data) {
const output = data[outputName];
const identifier = getOutputIdentifier(output, outputName);
kdlContent += `output "${identifier}" {\n`;
if (output.current_mode !== undefined && output.modes && output.modes[output.current_mode]) {
const mode = output.modes[output.current_mode];
kdlContent += ` mode "${mode.width}x${mode.height}@${(mode.refresh_rate / 1000).toFixed(3)}"\n`;
}
if (output.logical) {
if (output.logical.scale && output.logical.scale !== 1.0) {
kdlContent += ` scale ${output.logical.scale}\n`;
}
if (output.logical.transform && output.logical.transform !== "Normal") {
const transformMap = {
"Normal": "normal",
"90": "90",
"180": "180",
"270": "270",
"Flipped": "flipped",
"Flipped90": "flipped-90",
"Flipped180": "flipped-180",
"Flipped270": "flipped-270"
};
kdlContent += ` transform "${transformMap[output.logical.transform] || "normal"}"\n`;
}
if (output.logical.x !== undefined && output.logical.y !== undefined) {
kdlContent += ` position x=${output.logical.x} y=${output.logical.y}\n`;
}
}
if (output.vrr_enabled) {
kdlContent += ` variable-refresh-rate\n`;
}
kdlContent += `}\n\n`;
}
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms";
const outputsPath = niriDmsDir + "/outputs.kdl";
Proc.runCommand("niri-write-outputs", ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${kdlContent}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("NiriService: Failed to write outputs config:", output);
return;
}
console.info("NiriService: Generated outputs config at", outputsPath);
});
}
IpcHandler { IpcHandler {
function screenshot(): string { function screenshot(): string {
if (!CompositorService.isNiri) { if (!CompositorService.isNiri) {
@@ -6,6 +6,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
@@ -20,6 +21,7 @@ Singleton {
} }
return false; return false;
} }
readonly property bool shouldPauseCycling: anyFullscreen || SessionService.locked
property string cachedCyclingTime: SessionData.wallpaperCyclingTime property string cachedCyclingTime: SessionData.wallpaperCyclingTime
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
property string lastTimeCheck: "" property string lastTimeCheck: ""
@@ -34,7 +36,7 @@ Singleton {
running: false running: false
repeat: true repeat: true
onTriggered: { onTriggered: {
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.anyFullscreen) { if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.shouldPauseCycling) {
WallpaperCyclingService.cycleNextForMonitor(targetScreen); WallpaperCyclingService.cycleNextForMonitor(targetScreen);
} }
} }
@@ -113,6 +115,16 @@ Singleton {
} }
} }
Connections {
target: SessionService
function onSessionUnlocked() {
if (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper) {
updateCyclingState();
}
}
}
function updateCyclingState() { function updateCyclingState() {
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
stopCycling(); stopCycling();
@@ -151,13 +163,18 @@ Singleton {
} }
function startCycling() { function startCycling() {
if (SessionData.wallpaperCyclingMode === "interval") { switch (SessionData.wallpaperCyclingMode) {
case "interval":
lastTimeCheck = "";
intervalTimer.interval = cachedCyclingInterval * 1000; intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.start(); intervalTimer.start();
cyclingActive = true; cyclingActive = true;
} else if (SessionData.wallpaperCyclingMode === "time") { break;
case "time":
intervalTimer.stop();
cyclingActive = true; cyclingActive = true;
checkTimeBasedCycling(); checkTimeBasedCycling();
break;
} }
} }
@@ -167,7 +184,13 @@ Singleton {
} }
function startMonitorCycling(screenName, settings) { function startMonitorCycling(screenName, settings) {
if (settings.mode === "interval") { switch (settings.mode) {
case "interval":
{
var newChecks = Object.assign({}, monitorLastTimeChecks);
delete newChecks[screenName];
monitorLastTimeChecks = newChecks;
var timer = monitorTimers[screenName]; var timer = monitorTimers[screenName];
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) { if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
var newTimers = Object.assign({}, monitorTimers); var newTimers = Object.assign({}, monitorTimers);
@@ -180,10 +203,24 @@ Singleton {
timer.interval = settings.interval * 1000; timer.interval = settings.interval * 1000;
timer.start(); timer.start();
} }
} else if (settings.mode === "time") { break;
}
case "time":
{
var existingTimer = monitorTimers[screenName];
if (existingTimer) {
existingTimer.stop();
existingTimer.destroy();
var newTimers = Object.assign({}, monitorTimers);
delete newTimers[screenName];
monitorTimers = newTimers;
}
var newChecks = Object.assign({}, monitorLastTimeChecks); var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = ""; newChecks[screenName] = "";
monitorLastTimeChecks = newChecks; monitorLastTimeChecks = newChecks;
break;
}
} }
} }
@@ -319,7 +356,7 @@ Singleton {
} }
function checkTimeBasedCycling() { function checkTimeBasedCycling() {
if (anyFullscreen) if (shouldPauseCycling)
return; return;
const currentTime = Qt.formatTime(systemClock.date, "hh:mm"); const currentTime = Qt.formatTime(systemClock.date, "hh:mm");
@@ -367,7 +404,7 @@ Singleton {
running: false running: false
repeat: true repeat: true
onTriggered: { onTriggered: {
if (anyFullscreen) if (shouldPauseCycling)
return; return;
cycleToNextWallpaper(); cycleToNextWallpaper();
} }
+5
View File
@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layer-rule { layer-rule {
match namespace="dms:blurwallpaper" match namespace="dms:blurwallpaper"
place-within-backdrop true place-within-backdrop true
+1 -1
View File
@@ -74,7 +74,7 @@ PanelWindow {
readonly property real dpr: CompositorService.getScreenScale(screen) readonly property real dpr: CompositorService.getScreenScale(screen)
readonly property real screenWidth: screen.width readonly property real screenWidth: screen.width
readonly property real screenHeight: screen.height readonly property real screenHeight: screen.height
readonly property real shadowBuffer: 5 readonly property real shadowBuffer: 15
readonly property real alignedWidth: Theme.px(osdWidth, dpr) readonly property real alignedWidth: Theme.px(osdWidth, dpr)
readonly property real alignedHeight: Theme.px(osdHeight, dpr) readonly property real alignedHeight: Theme.px(osdHeight, dpr)
+15 -16
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -13,7 +12,7 @@ StyledRect {
onActiveFocusChanged: { onActiveFocusChanged: {
if (activeFocus) { if (activeFocus) {
textInput.forceActiveFocus() textInput.forceActiveFocus();
} }
} }
@@ -53,26 +52,26 @@ StyledRect {
signal focusStateChanged(bool hasFocus) signal focusStateChanged(bool hasFocus)
function getActiveFocus() { function getActiveFocus() {
return textInput.activeFocus return textInput.activeFocus;
} }
function setFocus(value) { function setFocus(value) {
textInput.focus = value textInput.focus = value;
} }
function forceActiveFocus() { function forceActiveFocus() {
textInput.forceActiveFocus() textInput.forceActiveFocus();
} }
function selectAll() { function selectAll() {
textInput.selectAll() textInput.selectAll();
} }
function clear() { function clear() {
textInput.clear() textInput.clear();
} }
function insertText(str) { function insertText(str) {
textInput.insert(textInput.cursorPosition, str) textInput.insert(textInput.cursorPosition, str);
} }
width: 200 width: 200
height: 48 height: Math.round(Theme.fontSizeMedium * 3.4)
radius: cornerRadius radius: cornerRadius
color: backgroundColor color: backgroundColor
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
@@ -113,25 +112,25 @@ StyledRect {
Keys.forwardTo: root.keyForwardTargets Keys.forwardTo: root.keyForwardTargets
Keys.onLeftPressed: event => { Keys.onLeftPressed: event => {
if (root.ignoreLeftRightKeys) { if (root.ignoreLeftRightKeys) {
event.accepted = true event.accepted = true;
} else { } else {
// Allow normal TextInput cursor movement // Allow normal TextInput cursor movement
event.accepted = false event.accepted = false;
} }
} }
Keys.onRightPressed: event => { Keys.onRightPressed: event => {
if (root.ignoreLeftRightKeys) { if (root.ignoreLeftRightKeys) {
event.accepted = true event.accepted = true;
} else { } else {
event.accepted = false event.accepted = false;
} }
} }
Keys.onPressed: event => { Keys.onPressed: event => {
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) { if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false event.accepted = false;
for (var i = 0; i < root.keyForwardTargets.length; i++) { for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i]) { if (root.keyForwardTargets[i]) {
root.keyForwardTargets[i].Keys.pressed(event) root.keyForwardTargets[i].Keys.pressed(event);
} }
} }
} }
@@ -171,7 +170,7 @@ StyledRect {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
textInput.text = "" textInput.text = "";
} }
} }
} }
+67 -61
View File
@@ -54,6 +54,12 @@ Item {
readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : [] readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
readonly property bool hasConflict: _conflicts.length > 0 readonly property bool hasConflict: _conflicts.length > 0
readonly property real _inputHeight: Math.round(Theme.fontSizeMedium * 3)
readonly property real _chipHeight: Math.round(Theme.fontSizeSmall * 2.3)
readonly property real _buttonHeight: Math.round(Theme.fontSizeMedium * 2.3)
readonly property real _keysColumnWidth: Math.round(Theme.fontSizeSmall * 12)
readonly property real _labelWidth: Math.round(Theme.fontSizeSmall * 5)
signal toggleExpand signal toggleExpand
signal saveBind(string originalKey, var newData) signal saveBind(string originalKey, var newData)
signal removeBind(string key) signal removeBind(string key)
@@ -223,7 +229,7 @@ Item {
Rectangle { Rectangle {
id: collapsedRect id: collapsedRect
width: parent.width width: parent.width
height: Math.max(52, keysColumn.implicitHeight + Theme.spacingM * 2) height: Math.max(root._inputHeight + Theme.spacingM, keysColumn.implicitHeight + Theme.spacingM * 2)
radius: root.isExpanded ? 0 : Theme.cornerRadius radius: root.isExpanded ? 0 : Theme.cornerRadius
topLeftRadius: Theme.cornerRadius topLeftRadius: Theme.cornerRadius
topRightRadius: Theme.cornerRadius topRightRadius: Theme.cornerRadius
@@ -240,7 +246,7 @@ Item {
Column { Column {
id: keysColumn id: keysColumn
Layout.preferredWidth: 140 Layout.preferredWidth: root._keysColumnWidth
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
@@ -253,9 +259,9 @@ Item {
property bool isSelected: root.isExpanded && root.editingKeyIndex === index && !root.addingNewKey property bool isSelected: root.isExpanded && root.editingKeyIndex === index && !root.addingNewKey
width: 140 width: root._keysColumnWidth
height: 28 height: root._chipHeight
radius: 6 radius: root._chipHeight / 4
color: isSelected ? Theme.primary : Theme.surfaceVariant color: isSelected ? Theme.primary : Theme.surfaceVariant
Rectangle { Rectangle {
@@ -332,7 +338,7 @@ Item {
DankIcon { DankIcon {
name: "warning" name: "warning"
size: 14 size: Theme.iconSizeSmall
color: Theme.primary color: Theme.primary
visible: root.hasConfigConflict visible: root.hasConfigConflict
} }
@@ -352,7 +358,7 @@ Item {
DankIcon { DankIcon {
name: root.isExpanded ? "expand_less" : "expand_more" name: root.isExpanded ? "expand_less" : "expand_more"
size: 20 size: Theme.iconSize - 4
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -360,7 +366,7 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 140 + Theme.spacingM * 2 anchors.leftMargin: root._keysColumnWidth + Theme.spacingM * 2
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.toggleExpand() onClicked: root.toggleExpand()
} }
@@ -420,7 +426,7 @@ Item {
DankIcon { DankIcon {
name: "warning" name: "warning"
size: 16 size: Theme.iconSizeSmall
color: Theme.primary color: Theme.primary
} }
@@ -461,7 +467,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
Flow { Flow {
@@ -478,8 +484,8 @@ Item {
property bool isSelected: root.editingKeyIndex === index && !root.addingNewKey property bool isSelected: root.editingKeyIndex === index && !root.addingNewKey
width: editKeyChipText.implicitWidth + Theme.spacingM width: editKeyChipText.implicitWidth + Theme.spacingM
height: 28 height: root._chipHeight
radius: 6 radius: root._chipHeight / 4
color: isSelected ? Theme.primary : Theme.surfaceVariant color: isSelected ? Theme.primary : Theme.surfaceVariant
Rectangle { Rectangle {
@@ -509,9 +515,9 @@ Item {
} }
Rectangle { Rectangle {
width: 28 width: root._chipHeight
height: 28 height: root._chipHeight
radius: 6 radius: root._chipHeight / 4
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: !root.isNew visible: !root.isNew
@@ -523,7 +529,7 @@ Item {
DankIcon { DankIcon {
name: "add" name: "add"
size: 16 size: Theme.iconSizeSmall
color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
anchors.centerIn: parent anchors.centerIn: parent
} }
@@ -548,13 +554,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
FocusScope { FocusScope {
id: captureScope id: captureScope
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
focus: root.recording focus: root.recording
Component.onCompleted: { Component.onCompleted: {
@@ -596,12 +602,12 @@ Item {
DankActionButton { DankActionButton {
id: recordBtn id: recordBtn
width: 28 width: root._chipHeight
height: 28 height: root._chipHeight
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
circular: false circular: false
iconName: root.recording ? "close" : "radio_button_checked" iconName: root.recording ? "close" : "radio_button_checked"
iconSize: 16 iconSize: Theme.iconSizeSmall
iconColor: root.recording ? Theme.error : Theme.primary iconColor: root.recording ? Theme.error : Theme.primary
onClicked: root.recording ? root.stopRecording() : root.startRecording() onClicked: root.recording ? root.stopRecording() : root.startRecording()
} }
@@ -703,8 +709,8 @@ Item {
} }
Rectangle { Rectangle {
Layout.preferredWidth: 40 Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: root.keys.length === 1 && !root.isNew visible: root.keys.length === 1 && !root.isNew
@@ -717,7 +723,7 @@ Item {
DankIcon { DankIcon {
name: "add" name: "add"
size: 18 size: Theme.iconSizeSmall + 2
color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
anchors.centerIn: parent anchors.centerIn: parent
} }
@@ -736,11 +742,11 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingS spacing: Theme.spacingS
visible: root.hasConflict visible: root.hasConflict
Layout.leftMargin: 60 + Theme.spacingM Layout.leftMargin: root._labelWidth + Theme.spacingM
DankIcon { DankIcon {
name: "warning" name: "warning"
size: 16 size: Theme.iconSizeSmall
color: Theme.primary color: Theme.primary
} }
@@ -762,7 +768,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
RowLayout { RowLayout {
@@ -785,7 +791,7 @@ Item {
}) })
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 36 Layout.preferredHeight: root._buttonHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent") border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
@@ -797,7 +803,7 @@ Item {
DankIcon { DankIcon {
name: typeDelegate.modelData.icon name: typeDelegate.modelData.icon
size: 16 size: Theme.iconSizeSmall
color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
} }
@@ -869,7 +875,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
DankDropdown { DankDropdown {
@@ -913,14 +919,14 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
visible: dmsArgsRow.hasAmountArg visible: dmsArgsRow.hasAmountArg
} }
DankTextField { DankTextField {
id: dmsAmountField id: dmsAmountField
Layout.preferredWidth: 80 Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 5.5)
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
placeholderText: "5" placeholderText: "5"
visible: dmsArgsRow.hasAmountArg visible: dmsArgsRow.hasAmountArg
@@ -961,14 +967,14 @@ Item {
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.leftMargin: dmsArgsRow.hasAmountArg ? Theme.spacingM : 0 Layout.leftMargin: dmsArgsRow.hasAmountArg ? Theme.spacingM : 0
Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : 60 Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : root._labelWidth
visible: dmsArgsRow.hasDeviceArg visible: dmsArgsRow.hasDeviceArg
} }
DankTextField { DankTextField {
id: dmsDeviceField id: dmsDeviceField
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("leave empty for default") placeholderText: I18n.tr("leave empty for default")
visible: dmsArgsRow.hasDeviceArg visible: dmsArgsRow.hasDeviceArg
@@ -1006,7 +1012,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
visible: dmsArgsRow.hasTabArg visible: dmsArgsRow.hasTabArg
} }
@@ -1064,12 +1070,12 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
DankDropdown { DankDropdown {
id: compositorCatDropdown id: compositorCatDropdown
Layout.preferredWidth: 120 Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 8.5)
compactMode: true compactMode: true
currentValue: { currentValue: {
const base = root.editAction.split(" ")[0]; const base = root.editAction.split(" ")[0];
@@ -1108,8 +1114,8 @@ Item {
} }
Rectangle { Rectangle {
Layout.preferredWidth: 40 Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
@@ -1121,7 +1127,7 @@ Item {
DankIcon { DankIcon {
name: "edit" name: "edit"
size: 18 size: Theme.iconSizeSmall + 2
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.centerIn: parent anchors.centerIn: parent
} }
@@ -1150,7 +1156,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
RowLayout { RowLayout {
@@ -1160,7 +1166,7 @@ Item {
DankTextField { DankTextField {
id: argValueField id: argValueField
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
visible: { visible: {
const cfg = optionsRow.argConfig; const cfg = optionsRow.argConfig;
if (!cfg?.config?.args) if (!cfg?.config?.args)
@@ -1308,13 +1314,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
DankTextField { DankTextField {
id: customCompositorField id: customCompositorField
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., focus-workspace 3, resize-column -10") placeholderText: I18n.tr("e.g., focus-workspace 3, resize-column -10")
text: root._actionType === "compositor" ? root.editAction : "" text: root._actionType === "compositor" ? root.editAction : ""
onTextChanged: { onTextChanged: {
@@ -1327,8 +1333,8 @@ Item {
} }
Rectangle { Rectangle {
Layout.preferredWidth: 40 Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
@@ -1340,7 +1346,7 @@ Item {
DankIcon { DankIcon {
name: "list" name: "list"
size: 18 size: Theme.iconSizeSmall + 2
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.centerIn: parent anchors.centerIn: parent
} }
@@ -1371,13 +1377,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
DankTextField { DankTextField {
id: spawnTextField id: spawnTextField
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., firefox, kitty --title foo") placeholderText: I18n.tr("e.g., firefox, kitty --title foo")
readonly property var _parsed: root._actionType === "spawn" ? Actions.parseSpawnCommand(root.editAction) : null readonly property var _parsed: root._actionType === "spawn" ? Actions.parseSpawnCommand(root.editAction) : null
text: _parsed ? (_parsed.command + " " + _parsed.args.join(" ")).trim() : "" text: _parsed ? (_parsed.command + " " + _parsed.args.join(" ")).trim() : ""
@@ -1403,13 +1409,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
DankTextField { DankTextField {
id: shellTextField id: shellTextField
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1") placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1")
text: root._actionType === "shell" ? Actions.parseShellCommand(root.editAction) : "" text: root._actionType === "shell" ? Actions.parseShellCommand(root.editAction) : ""
onTextChanged: { onTextChanged: {
@@ -1431,13 +1437,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
DankTextField { DankTextField {
id: titleField id: titleField
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("Hotkey overlay title (optional)") placeholderText: I18n.tr("Hotkey overlay title (optional)")
text: root.editDesc text: root.editDesc
onTextChanged: root.updateEdit({ onTextChanged: root.updateEdit({
@@ -1455,13 +1461,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.preferredWidth: 60 Layout.preferredWidth: root._labelWidth
} }
DankTextField { DankTextField {
id: cooldownField id: cooldownField
Layout.preferredWidth: 100 Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 7)
Layout.preferredHeight: 40 Layout.preferredHeight: root._inputHeight
placeholderText: "0" placeholderText: "0"
Connections { Connections {
@@ -1508,8 +1514,8 @@ Item {
spacing: Theme.spacingM spacing: Theme.spacingM
DankActionButton { DankActionButton {
Layout.preferredWidth: 32 Layout.preferredWidth: root._buttonHeight
Layout.preferredHeight: 32 Layout.preferredHeight: root._buttonHeight
circular: false circular: false
iconName: "delete" iconName: "delete"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
@@ -1531,7 +1537,7 @@ Item {
DankButton { DankButton {
text: I18n.tr("Cancel") text: I18n.tr("Cancel")
buttonHeight: 32 buttonHeight: root._buttonHeight
backgroundColor: Theme.surfaceContainer backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText textColor: Theme.surfaceText
visible: root.hasChanges || root.isNew visible: root.hasChanges || root.isNew
@@ -1547,7 +1553,7 @@ Item {
DankButton { DankButton {
text: root.isNew ? I18n.tr("Add") : I18n.tr("Save") text: root.isNew ? I18n.tr("Add") : I18n.tr("Save")
buttonHeight: 32 buttonHeight: root._buttonHeight
enabled: root.canSave() enabled: root.canSave()
visible: root.hasChanges || root.isNew visible: root.hasChanges || root.isNew
onClicked: root.doSave() onClicked: root.doSave()
+28 -284
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
@@ -13,7 +14,7 @@ Rectangle {
property string expandedUuid: "" property string expandedUuid: ""
property int listHeight: 180 property int listHeight: 180
implicitHeight: contentColumn.implicitHeight + Theme.spacingM * 2 implicitHeight: 32 + 1 + listHeight + Theme.spacingS * 4 + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -153,25 +154,14 @@ Rectangle {
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
} }
DankFlickable {
width: parent.width
height: root.listHeight
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: 4
Item { Item {
width: parent.width width: parent.width
height: DMSNetworkService.profiles.length === 0 ? 100 : 0 height: root.listHeight
visible: height > 0
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
visible: DMSNetworkService.profiles.length === 0
DankIcon { DankIcon {
name: "vpn_key_off" name: "vpn_key_off"
@@ -194,292 +184,46 @@ Rectangle {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
}
Repeater { DankListView {
model: DMSNetworkService.profiles id: vpnListView
anchors.fill: parent
delegate: Rectangle { visible: DMSNetworkService.profiles.length > 0
id: profileRow spacing: 4
required property var modelData cacheBuffer: 200
required property int index
readonly property bool isActive: DMSNetworkService.isActiveUuid(modelData.uuid)
readonly property bool isExpanded: root.expandedUuid === modelData.uuid
readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse
readonly property var configData: isExpanded ? VPNService.editConfig : null
width: listCol.width
height: isExpanded ? 46 + expandedContent.height : 46
radius: Theme.cornerRadius
color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
border.width: isActive ? 2 : 1
border.color: isActive ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
clip: true clip: true
Behavior on height { model: ScriptModel {
NumberAnimation { values: DMSNetworkService.profiles
duration: 150 objectProp: "uuid"
easing.type: Easing.OutQuad
}
} }
MouseArea { delegate: VpnProfileDelegate {
id: rowArea required property var modelData
anchors.fill: parent width: vpnListView.width
hoverEnabled: true profile: modelData
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor isExpanded: root.expandedUuid === modelData.uuid
enabled: !DMSNetworkService.isBusy onToggleExpand: {
onClicked: DMSNetworkService.toggle(modelData.uuid) if (root.expandedUuid === modelData.uuid) {
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
Row {
width: parent.width
height: 46 - Theme.spacingS * 2
spacing: Theme.spacingS
DankIcon {
name: isActive ? "vpn_lock" : "vpn_key_off"
size: 20
color: isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 1
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: VPNService.getVpnTypeFromProfile(modelData)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.NoWrap
width: parent.width
elide: Text.ElideRight
}
}
Item {
width: Theme.spacingXS
height: 1
}
Rectangle {
id: expandBtnRect
width: 28
height: 28
radius: 14
color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: expandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (isExpanded) {
root.expandedUuid = ""; root.expandedUuid = "";
} else { return;
}
root.expandedUuid = modelData.uuid; root.expandedUuid = modelData.uuid;
VPNService.getConfig(modelData.uuid); VPNService.getConfig(modelData.uuid);
} }
} onDeleteRequested: {
}
}
Rectangle {
id: deleteBtnRect
width: 28
height: 28
radius: 14
color: deleteBtn.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 18
color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
deleteConfirm.showWithOptions({ deleteConfirm.showWithOptions({
title: I18n.tr("Delete VPN"), "title": I18n.tr("Delete VPN"),
message: I18n.tr("Delete \"") + modelData.name + "\"?", "message": I18n.tr("Delete \"") + modelData.name + "\"?",
confirmText: I18n.tr("Delete"), "confirmText": I18n.tr("Delete"),
confirmColor: Theme.error, "confirmColor": Theme.error,
onConfirm: () => VPNService.deleteVpn(modelData.uuid) "onConfirm": () => VPNService.deleteVpn(modelData.uuid)
}); });
} }
} }
} }
} }
Column {
id: expandedContent
width: parent.width
spacing: Theme.spacingXS
visible: isExpanded
Rectangle {
width: parent.width
height: 1
color: Theme.outlineLight
}
Item {
width: parent.width
height: VPNService.configLoading ? 40 : 0
visible: VPNService.configLoading
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "sync"
size: 16
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("Loading...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: !VPNService.configLoading && configData
Repeater {
model: {
if (!configData)
return [];
const fields = [];
const data = configData.data || {};
if (data.remote)
fields.push({
label: I18n.tr("Server"),
value: data.remote
});
if (configData.username || data.username)
fields.push({
label: I18n.tr("Username"),
value: configData.username || data.username
});
if (data.cipher)
fields.push({
label: I18n.tr("Cipher"),
value: data.cipher
});
if (data.auth)
fields.push({
label: I18n.tr("Auth"),
value: data.auth
});
if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no")
fields.push({
label: I18n.tr("Protocol"),
value: data["proto-tcp"] === "yes" ? "TCP" : "UDP"
});
if (data["tunnel-mtu"])
fields.push({
label: I18n.tr("MTU"),
value: data["tunnel-mtu"]
});
if (data["connection-type"])
fields.push({
label: I18n.tr("Auth Type"),
value: data["connection-type"]
});
fields.push({
label: I18n.tr("Autoconnect"),
value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
});
return fields;
}
delegate: Rectangle {
required property var modelData
required property int index
width: fieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: fieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Item {
width: 1
height: Theme.spacingXS
}
}
}
}
}
}
}
Item { Item {
width: 1 width: 1
height: Theme.spacingS height: Theme.spacingS
+275
View File
@@ -0,0 +1,275 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
required property var profile
property bool isExpanded: false
signal toggleExpand
signal deleteRequested
readonly property bool isActive: DMSNetworkService.activeUuids?.includes(profile?.uuid) ?? false
readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse
readonly property var configData: isExpanded ? VPNService.editConfig : null
readonly property var configFields: buildConfigFields()
height: isExpanded ? 46 + expandedContent.height : 46
radius: Theme.cornerRadius
color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
border.width: isActive ? 2 : 1
border.color: isActive ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
clip: true
function buildConfigFields() {
if (!configData)
return [];
const fields = [];
const data = configData.data || {};
if (data.remote)
fields.push({
"key": "server",
"label": I18n.tr("Server"),
"value": data.remote
});
if (configData.username || data.username)
fields.push({
"key": "user",
"label": I18n.tr("Username"),
"value": configData.username || data.username
});
if (data.cipher)
fields.push({
"key": "cipher",
"label": I18n.tr("Cipher"),
"value": data.cipher
});
if (data.auth)
fields.push({
"key": "auth",
"label": I18n.tr("Auth"),
"value": data.auth
});
if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no")
fields.push({
"key": "proto",
"label": I18n.tr("Protocol"),
"value": data["proto-tcp"] === "yes" ? "TCP" : "UDP"
});
if (data["tunnel-mtu"])
fields.push({
"key": "mtu",
"label": I18n.tr("MTU"),
"value": data["tunnel-mtu"]
});
if (data["connection-type"])
fields.push({
"key": "conntype",
"label": I18n.tr("Auth Type"),
"value": data["connection-type"]
});
fields.push({
"key": "auto",
"label": I18n.tr("Autoconnect"),
"value": configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
});
return fields;
}
Behavior on height {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(profile.uuid)
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
Row {
width: parent.width
height: 46 - Theme.spacingS * 2
spacing: Theme.spacingS
DankIcon {
name: isActive ? "vpn_lock" : "vpn_key_off"
size: 20
color: isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 1
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4
StyledText {
text: profile?.name ?? ""
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: VPNService.getVpnTypeFromProfile(profile)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.NoWrap
width: parent.width
elide: Text.ElideRight
}
}
Item {
width: Theme.spacingXS
height: 1
}
Rectangle {
width: 28
height: 28
radius: 14
color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: isExpanded ? "expand_less" : "expand_more"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: expandBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleExpand()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: deleteBtn.containsMouse ? Theme.errorHover : "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "delete"
size: 18
color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteBtn
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.deleteRequested()
}
}
}
Column {
id: expandedContent
width: parent.width
spacing: Theme.spacingXS
visible: isExpanded
Rectangle {
width: parent.width
height: 1
color: Theme.outlineLight
}
Item {
width: parent.width
height: VPNService.configLoading ? 40 : 0
visible: VPNService.configLoading
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "sync"
size: 16
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("Loading...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Flow {
width: parent.width
spacing: Theme.spacingXS
visible: !VPNService.configLoading && configData
Repeater {
model: configFields
delegate: Rectangle {
required property var modelData
width: fieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
Row {
id: fieldContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: modelData.label + ":"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.value
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Item {
width: 1
height: Theme.spacingXS
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More