1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-05 03:52:07 -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
.github/workflows/backup/run-obs.yml.bak vendored Normal file
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
.github/workflows/backup/run-ppa.yml.bak vendored Normal file
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

View File

@@ -4,18 +4,18 @@ on:
workflow_dispatch:
inputs:
package:
description: 'Package to update (dms, dms-git, or all)'
description: "Package to update (dms, dms-git, or all)"
required: false
default: 'all'
default: "all"
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
default: ''
default: ""
push:
tags:
- 'v*'
- "v*"
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
@@ -33,78 +33,114 @@ jobs:
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
env:
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
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
# Tag push - always update stable package
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
# Scheduled run - check dms-git only
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
else
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow trigger
PKG="${{ github.event.inputs.package }}"
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -n "$REBUILD" ]]; then
# Rebuild requested - always proceed
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
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")
# 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
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No spec file in OBS, proceeding with update"
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
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
cd "${{ github.workspace }}"
else
# Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
echo "Manual trigger: $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
# Fallback - proceed
echo "packages=all" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
@@ -113,9 +149,7 @@ jobs:
name: Upload to OBS
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
if: needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
@@ -135,8 +169,14 @@ jobs:
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 }}"
# 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 "Manual trigger: ${{ github.event.inputs.package }}"
fi
else
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
@@ -144,66 +184,42 @@ jobs:
- name: Update dms-git spec version
if: 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
# Single changelog entry (git snapshots don't need history)
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
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' 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
if: 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"
# Single changelog entry (git snapshots don't need history)
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
{
echo "dms-git ($NEW_VERSION) nightly; urgency=medium"
echo ""
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog"
- name: Update dms stable version
if: steps.packages.outputs.version != ''
@@ -212,13 +228,17 @@ jobs:
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
# Single changelog entry (full history on OBS website)
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
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' 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)
for service in distro/debian/*/_service; do
@@ -232,31 +252,23 @@ jobs:
fi
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
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 "dms ($VERSION_NO_V) stable; urgency=medium"
echo ""
echo " * Update to $VERSION stable release"
echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms/debian/changelog"
echo "✓ Updated Debian changelog to $VERSION_NO_V"
fi
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: "1.24"
- name: Install OSC
run: |
@@ -276,32 +288,76 @@ jobs:
- name: Upload to OBS
env:
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
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
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
# 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
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"
else
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
fi
done
- name: Summary
if: always()
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
echo "### OBS Package Upload Summary" >> $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
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
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $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

View File

@@ -4,15 +4,15 @@ on:
workflow_dispatch:
inputs:
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
default: 'dms-git'
default: "dms-git"
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
default: ''
default: ""
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
jobs:
check-updates:
@@ -32,41 +32,112 @@ jobs:
- 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..."
# 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 "")
# 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
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 "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
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
# Scheduled run - check dms-git only
echo "packages=dms-git" >> $GITHUB_OUTPUT
if check_dms_git; then
echo "has_updates=true" >> $GITHUB_OUTPUT
else
echo "has_updates=false" >> $GITHUB_OUTPUT
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 }}"
# 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 "🔄 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
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 "has_updates=true" >> $GITHUB_OUTPUT
fi
@@ -75,9 +146,7 @@ jobs:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
if: needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
@@ -88,7 +157,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: "1.24"
cache: false
- name: Install build dependencies
@@ -114,81 +183,97 @@ jobs:
- name: Determine packages to upload
id: packages
run: |
# Use packages determined by check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
if [[ "${{ 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
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
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
fi
- name: Upload to PPA
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
# Export to ensure it's available to subprocesses
if [ -n "$REBUILD_RELEASE" ]; then
if [[ -z "$PACKAGES" ]]; 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
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
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 "Uploading dms to PPA..."
if [ -n "$REBUILD_RELEASE" ]; then
echo "Uploading $PKG to PPA $PPA_NAME..."
if [[ -n "$REBUILD_RELEASE" ]]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
REBUILD_RELEASE="$REBUILD_RELEASE" bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
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
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
done
- name: Summary
if: always()
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "### PPA Package Upload Summary" >> $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
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 will complete 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://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
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY

19
.github/workflows/stable.yml vendored Normal file
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
.gitignore vendored
View File

@@ -104,12 +104,6 @@ go.work.sum
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
.envrc
.direnv/

View File

@@ -2,6 +2,15 @@ repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.6.2
hooks:
- id: golangci-lint-full
- id: golangci-lint-fmt
require_serial: true
- id: golangci-lint-full
- id: golangci-lint-config-verify
- repo: local
hooks:
- id: go-test
name: go test
entry: go test ./...
language: system
pass_filenames: false
types: [go]

View File

@@ -15,6 +15,7 @@ import (
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"hash/fnv"
bolt "go.etcd.io/bbolt"
)
@@ -39,6 +40,7 @@ type Entry struct {
Size int
Timestamp time.Time
IsImage bool
Hash uint64
}
func Store(data []byte, mimeType string) error {
@@ -70,6 +72,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
Size: len(data),
Timestamp: time.Now(),
IsImage: IsImageMimeType(mimeType),
Hash: computeHash(data),
}
switch {
@@ -85,7 +88,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
return err
}
if err := deduplicateInTx(b, data); err != nil {
if err := deduplicateInTx(b, entry.Hash); err != nil {
return err
}
@@ -126,17 +129,14 @@ func getDBPath() (string, error) {
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()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
if err != nil {
if extractHash(v) != hash {
continue
}
if bytes.Equal(entry.Data, data) {
if err := b.Delete(k); err != nil {
return err
}
if err := b.Delete(k); err != nil {
return err
}
}
return nil
@@ -174,54 +174,30 @@ func encodeEntry(e Entry) ([]byte, error) {
} else {
buf.WriteByte(0)
}
binary.Write(buf, binary.BigEndian, e.Hash)
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 {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v)
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 {
text := string(data)
text = strings.TrimSpace(text)

View File

@@ -276,4 +276,4 @@ bind = CTRL, Print, exec, dms screenshot full
bind = ALT, Print, exec, dms screenshot window
# === System Controls ===
bind = $mod SHIFT, P, dpms, off
bind = $mod SHIFT, P, dpms, toggle

View File

@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
recent-windows {
highlight {
corner-radius 12

View File

@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
binds {
// === System & Overview ===
Mod+D repeat=false { toggle-overview; }

View File

@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout {
background-color "transparent"

View File

@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layout {
gaps 4

View File

@@ -18,15 +18,64 @@ gestures {
input {
keyboard {
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
}
// 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 {
// 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 {
// off
// natural-scroll
// accel-speed 0.2
// accel-profile "flat"
// scroll-method "no-scroll"
}
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
// by running `niri msg outputs` while inside a niri instance.

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 {
if len(binds) == 0 {
return "binds {}\n"
return dmsWarningHeader + "binds {}\n"
}
var regularBinds, recentWindowsBinds []*overrideBind
@@ -490,6 +497,7 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
var sb strings.Builder
sb.WriteString(dmsWarningHeader)
sb.WriteString("binds {\n")
for _, bind := range regularBinds {
n.writeBindNode(&sb, bind, " ")

View File

@@ -6,6 +6,13 @@ import (
"testing"
)
const testHeader = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
`
func TestNiriProviderName(t *testing.T) {
provider := NewNiriProvider("")
if provider.Name() != "niri" {
@@ -197,7 +204,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
{
name: "empty binds",
binds: map[string]*overrideBind{},
expected: "binds {}\n",
expected: testHeader + "binds {}\n",
},
{
name: "simple spawn bind",
@@ -208,7 +215,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Open Terminal",
},
},
expected: `binds {
expected: testHeader + `binds {
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
}
`,
@@ -222,7 +229,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Application Launcher",
},
},
expected: `binds {
expected: testHeader + `binds {
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},
},
},
expected: `binds {
expected: testHeader + `binds {
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
}
`,
@@ -250,7 +257,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Description: "Close Window",
},
},
expected: `binds {
expected: testHeader + `binds {
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
}
`,
@@ -263,7 +270,7 @@ func TestNiriGenerateBindsContent(t *testing.T) {
Action: "next-window",
},
},
expected: `binds {
expected: testHeader + `binds {
}
recent-windows {
@@ -415,7 +422,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Focus Workspace 1",
},
},
expected: `binds {
expected: testHeader + `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
}
`,
@@ -429,7 +436,7 @@ func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
Description: "Focus Workspace 10",
},
},
expected: `binds {
expected: testHeader + `binds {
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%",
},
},
expected: `binds {
expected: testHeader + `binds {
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%",
},
},
expected: `binds {
expected: testHeader + `binds {
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)
expected := `binds {
expected := testHeader + `binds {
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)
expected := `binds {
expected := testHeader + `binds {
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)
expected := `binds {
expected := testHeader + `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`

View File

@@ -238,7 +238,7 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
if proxy == nil || proxy.IsZombie() {
head := &ZwlrOutputHeadV1{}
head.SetContext(i.Context())
head.SetID(objectID)
@@ -723,7 +723,7 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
if proxy == nil || proxy.IsZombie() {
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)
@@ -761,8 +761,8 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
l := 0
objectID := client.Uint32(data[l : l+4])
proxy := i.Context().GetProxy(objectID)
if proxy == nil {
// Mode not yet registered, create it
if proxy == nil || proxy.IsZombie() {
// Mode not yet registered or zombie, create fresh
mode := &ZwlrOutputModeV1{}
mode.SetContext(i.Context())
mode.SetID(objectID)

View File

@@ -18,6 +18,7 @@ import (
"github.com/fsnotify/fsnotify"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"hash/fnv"
bolt "go.etcd.io/bbolt"
@@ -69,6 +70,10 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
}
m.db = db
if err := m.migrateHashes(); err != nil {
log.Errorf("Failed to migrate hashes: %v", err)
}
if config.ClearAtStartup {
if err := m.clearHistoryInternal(); err != nil {
log.Errorf("Failed to clear history at startup: %v", err)
@@ -244,7 +249,30 @@ func (m *Manager) setupDataDeviceSync() {
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 {
@@ -262,77 +290,64 @@ func (m *Manager) setupDataDeviceSync() {
log.Info("Data device setup complete")
}
func (m *Manager) storeCurrentClipboard() {
if m.currentOffer == nil {
return
}
func (m *Manager) readAndStore(r *os.File, mimeType string) {
defer r.Close()
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
}
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 {
continue
}
allData[mime] = data
orderedMimes = append(orderedMimes, mime)
}
if len(allData) == 0 {
if len(data) == 0 || int64(len(data)) > cfg.MaxEntrySize {
return
}
preferredMime := m.selectMimeType(orderedMimes)
if preferredMime == "" {
preferredMime = orderedMimes[0]
}
data := allData[preferredMime]
if len(bytes.TrimSpace(data)) == 0 {
return
}
if !cfg.DisableHistory && m.db != nil {
entry := Entry{
Data: data,
MimeType: preferredMime,
Size: len(data),
Timestamp: time.Now(),
IsImage: m.isImageMimeType(preferredMime),
}
switch {
case entry.IsImage:
entry.Preview = m.imagePreview(data, preferredMime)
default:
entry.Preview = m.textPreview(data)
}
if err := m.storeEntry(entry); err != nil {
log.Errorf("Failed to store clipboard entry: %v", err)
}
m.storeClipboardEntry(data, mimeType)
}
if !cfg.DisablePersist {
m.persistClipboard(orderedMimes, allData)
m.persistClipboard([]string{mimeType}, map[string][]byte{mimeType: data})
}
m.updateState()
m.notifySubscribers()
}
func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
entry := Entry{
Data: data,
MimeType: mimeType,
Size: len(data),
Timestamp: time.Now(),
IsImage: m.isImageMimeType(mimeType),
}
switch {
case entry.IsImage:
entry.Preview = m.imagePreview(data, mimeType)
default:
entry.Preview = m.textPreview(data)
}
if err := m.storeEntry(entry); err != nil {
log.Errorf("Failed to store clipboard entry: %v", err)
}
}
func (m *Manager) persistClipboard(mimeTypes []string, data map[string][]byte) {
m.persistMutex.Lock()
m.persistMimeTypes = mimeTypes
@@ -415,14 +430,34 @@ func (m *Manager) takePersistOwnership() {
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 {
if m.db == nil {
return fmt.Errorf("database not available")
}
entry.Hash = computeHash(entry.Data)
return m.db.Update(func(tx *bolt.Tx) error {
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
}
@@ -446,17 +481,14 @@ 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()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
entry, err := decodeEntry(v)
if err != nil {
if extractHash(v) != hash {
continue
}
if bytes.Equal(entry.Data, data) {
if err := b.Delete(k); err != nil {
return err
}
if err := b.Delete(k); err != nil {
return err
}
}
return nil
@@ -494,6 +526,7 @@ func encodeEntry(e Entry) ([]byte, error) {
} else {
buf.WriteByte(0)
}
binary.Write(buf, binary.BigEndian, e.Hash)
return buf.Bytes(), nil
}
@@ -533,6 +566,10 @@ func decodeEntry(data []byte) (Entry, error) {
binary.Read(buf, binary.BigEndian, &isImage)
e.IsImage = isImage == 1
if buf.Len() >= 8 {
binary.Read(buf, binary.BigEndian, &e.Hash)
}
return e, nil
}
@@ -542,6 +579,19 @@ func itob(v uint64) []byte {
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 {
preferredTypes := []string{
"text/plain;charset=utf-8",
@@ -551,8 +601,9 @@ func (m *Manager) selectMimeType(mimes []string) string {
"TEXT",
"image/png",
"image/jpeg",
"image/bmp",
"image/gif",
"image/bmp",
"image/tiff",
}
for _, pref := range preferredTypes {
@@ -563,6 +614,10 @@ func (m *Manager) selectMimeType(mimes []string) string {
}
}
if len(mimes) > 0 {
return mimes[0]
}
return ""
}
@@ -570,37 +625,6 @@ func (m *Manager) isImageMimeType(mime string) bool {
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 {
text := string(data)
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 {
if m.db == nil {
return SearchResult{}
@@ -1224,23 +1321,6 @@ func (m *Manager) applyConfigChange(newCfg Config) {
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 {
cfg := m.getConfig()

View File

@@ -409,8 +409,10 @@ func TestSelectMimeType(t *testing.T) {
}{
{[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"},
{[]string{"text/html", "text/plain"}, "text/plain"},
{[]string{"application/json", "image/png"}, "image/png"},
{[]string{"application/json", "application/xml"}, ""},
{[]string{"text/html", "image/png"}, "image/png"},
{[]string{"image/png", "image/jpeg"}, "image/png"},
{[]string{"image/png"}, "image/png"},
{[]string{"application/octet-stream"}, "application/octet-stream"},
{[]string{}, ""},
}

View File

@@ -103,6 +103,7 @@ type Entry struct {
Size int `json:"size"`
Timestamp time.Time `json:"timestamp"`
IsImage bool `json:"isImage"`
Hash uint64 `json:"hash,omitempty"`
}
type State struct {
@@ -138,8 +139,9 @@ type Manager struct {
persistMimeTypes []string
persistMutex sync.RWMutex
isOwner bool
ownerLock sync.Mutex
isOwner bool
ownerLock sync.Mutex
initialized bool
alive bool

View File

@@ -11,4 +11,9 @@ const (
dbusPortalSettingsInterface = "org.freedesktop.portal.Settings"
dbusPropsInterface = "org.freedesktop.DBus.Properties"
dbusScreensaverName = "org.freedesktop.ScreenSaver"
dbusScreensaverPath = "/ScreenSaver"
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
)

View File

@@ -22,8 +22,9 @@ func NewManager() (*Manager, error) {
m := &Manager{
state: &FreedeskState{
Accounts: AccountsState{},
Settings: SettingsState{},
Accounts: AccountsState{},
Settings: SettingsState{},
Screensaver: ScreensaverState{},
},
stateMutex: sync.RWMutex{},
systemConn: systemConn,
@@ -33,6 +34,7 @@ func NewManager() (*Manager, error) {
m.initializeAccounts()
m.initializeSettings()
m.initializeScreensaver()
return m, nil
}

View File

@@ -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
})
}

View File

@@ -29,18 +29,35 @@ type SettingsState struct {
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 {
Accounts AccountsState `json:"accounts"`
Settings SettingsState `json:"settings"`
Accounts AccountsState `json:"accounts"`
Settings SettingsState `json:"settings"`
Screensaver ScreensaverState `json:"screensaver"`
}
type Manager struct {
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
state *FreedeskState
stateMutex sync.RWMutex
systemConn *dbus.Conn
sessionConn *dbus.Conn
accountsObj dbus.BusObject
settingsObj dbus.BusObject
currentUID uint64
subscribers syncmap.Map[string, chan FreedeskState]
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
screensaverCookieCounter uint32
}

View File

@@ -880,29 +880,24 @@ func (b *NetworkManagerBackend) ImportVPN(filePath string, name string) (*VPNImp
}
func (b *NetworkManagerBackend) importVPNWithNmcli(filePath string, name string) (*VPNImportResult, error) {
args := []string{"connection", "import", "type", "openvpn", "file", filePath}
cmd := exec.Command("nmcli", args...)
output, err := cmd.CombinedOutput()
vpnTypes := []string{"openvpn", "wireguard", "vpnc", "pptp", "l2tp", "openconnect", "strongswan"}
var output []byte
var err error
for _, vpnType := range vpnTypes {
args := []string{"connection", "import", "type", vpnType, "file", filePath}
cmd := exec.Command("nmcli", args...)
output, err = cmd.CombinedOutput()
if err == nil {
break
}
}
if err != nil {
outputStr := string(output)
if strings.Contains(outputStr, "vpnc") || strings.Contains(outputStr, "unknown connection type") {
for _, vpnType := range []string{"vpnc", "pptp", "l2tp", "openconnect", "strongswan", "wireguard"} {
args = []string{"connection", "import", "type", vpnType, "file", filePath}
cmd = exec.Command("nmcli", args...)
output, err = cmd.CombinedOutput()
if err == nil {
break
}
}
}
if err != nil {
return &VPNImportResult{
Success: false,
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
}, nil
}
return &VPNImportResult{
Success: false,
Error: fmt.Sprintf("import failed: %s", strings.TrimSpace(string(output))),
}, nil
}
outputStr := string(output)

View File

@@ -149,6 +149,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
}
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 {
models.RespondError(conn, req.ID, "clipboard manager not initialized")
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))
}
}
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"})
}

View File

@@ -33,7 +33,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const APIVersion = 23
const APIVersion = 24
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 {
wg.Add(1)
waylandChan := waylandManager.Subscribe(clientID + "-gamma")

View File

@@ -103,18 +103,16 @@ func (m *Manager) waylandActor() {
}
}
func (m *Manager) allOutputsReady() bool {
hasOutputs := false
allReady := true
func (m *Manager) anyOutputReady() bool {
anyReady := false
m.outputs.Range(func(_ uint32, out *outputState) bool {
hasOutputs = true
if out.rampSize == 0 || out.failed {
allReady = false
return false
if out.rampSize > 0 && !out.failed {
anyReady = true
return false // stop iteration
}
return true
})
return hasOutputs && allReady
return anyReady
}
func (m *Manager) setupDBusMonitor() error {
@@ -278,7 +276,8 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
out.failed = false
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)
case StateNormal:
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 {
@@ -588,7 +588,7 @@ func (m *Manager) schedulerLoop() {
m.configMutex.RUnlock()
if enabled {
m.post(func() { m.applyCurrentTemp() })
m.post(func() { m.applyCurrentTemp("startup") })
}
var timer *time.Timer
@@ -630,24 +630,27 @@ func (m *Manager) schedulerLoop() {
enabled := m.config.Enabled
m.configMutex.RUnlock()
if enabled {
m.post(func() { m.applyCurrentTemp() })
m.post(func() { m.applyCurrentTemp("updateTrigger") })
}
case <-timer.C:
m.configMutex.RLock()
enabled := m.config.Enabled
m.configMutex.RUnlock()
if enabled {
m.post(func() { m.applyCurrentTemp() })
m.post(func() { m.applyCurrentTemp("timer") })
}
}
}
}
func (m *Manager) applyCurrentTemp() {
if !m.controlsInitialized || !m.allOutputsReady() {
func (m *Manager) applyCurrentTemp(_ string) {
if !m.controlsInitialized || !m.anyOutputReady() {
return
}
// Ensure schedule is up-to-date (handles display wake after overnight sleep)
m.recalcSchedule(time.Now())
m.configMutex.RLock()
low, high := m.config.LowTemp, m.config.HighTemp
m.configMutex.RUnlock()
@@ -680,6 +683,10 @@ func (m *Manager) applyGamma(temp int) {
return
}
if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma {
return
}
var outs []*outputState
m.outputs.Range(func(_ uint32, out *outputState) bool {
outs = append(outs, out)
@@ -715,6 +722,7 @@ func (m *Manager) applyGamma(temp int) {
for _, j := range jobs {
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.rampSize = 0
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 {
@@ -901,6 +912,10 @@ func (m *Manager) SetConfig(config Config) error {
func (m *Manager) SetTemperature(low, high int) error {
m.configMutex.Lock()
if m.config.LowTemp == low && m.config.HighTemp == high {
m.configMutex.Unlock()
return nil
}
m.config.LowTemp = low
m.config.HighTemp = high
err := m.config.Validate()
@@ -914,6 +929,11 @@ func (m *Manager) SetTemperature(low, high int) error {
func (m *Manager) SetLocation(lat, lon float64) error {
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.Longitude = &lon
m.config.UseIPLocation = false
@@ -928,6 +948,10 @@ func (m *Manager) SetLocation(lat, lon float64) error {
func (m *Manager) SetUseIPLocation(use bool) {
m.configMutex.Lock()
if m.config.UseIPLocation == use {
m.configMutex.Unlock()
return
}
m.config.UseIPLocation = use
if use {
m.config.Latitude = nil
@@ -946,6 +970,12 @@ func (m *Manager) SetUseIPLocation(use bool) {
func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
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.ManualSunset = &sunset
err := m.config.Validate()
@@ -959,6 +989,10 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
func (m *Manager) ClearManualTimes() {
m.configMutex.Lock()
if m.config.ManualSunrise == nil && m.config.ManualSunset == nil {
m.configMutex.Unlock()
return
}
m.config.ManualSunrise = nil
m.config.ManualSunset = nil
m.configMutex.Unlock()
@@ -967,6 +1001,10 @@ func (m *Manager) ClearManualTimes() {
func (m *Manager) SetGamma(gamma float64) error {
m.configMutex.Lock()
if m.config.Gamma == gamma {
m.configMutex.Unlock()
return nil
}
m.config.Gamma = gamma
err := m.config.Validate()
m.configMutex.Unlock()
@@ -980,6 +1018,10 @@ func (m *Manager) SetGamma(gamma float64) error {
func (m *Manager) SetEnabled(enabled bool) {
m.configMutex.Lock()
wasEnabled := m.config.Enabled
if wasEnabled == enabled {
m.configMutex.Unlock()
return
}
m.config.Enabled = enabled
highTemp := m.config.HighTemp
m.configMutex.Unlock()
@@ -989,7 +1031,7 @@ func (m *Manager) SetEnabled(enabled bool) {
m.post(func() {
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
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
}
m.controlsInitialized = true

View File

@@ -96,6 +96,9 @@ type Manager struct {
dbusConn *dbus.Conn
dbusSignal chan *dbus.Signal
lastAppliedTemp int
lastAppliedGamma float64
}
type outputState struct {

View File

@@ -1,11 +1,11 @@
package wlcontext
import (
"errors"
"fmt"
"os"
"sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -27,6 +27,8 @@ type SharedContext struct {
stopChan chan struct{}
fatalError chan error
cmdQueue chan func()
wakeR int
wakeW int
wg sync.WaitGroup
mu sync.Mutex
started bool
@@ -38,11 +40,31 @@ func New() (*SharedContext, error) {
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{
display: display,
stopChan: make(chan struct{}),
fatalError: make(chan error, 1),
cmdQueue: make(chan func(), 256),
wakeR: fds[0],
wakeW: fds[1],
started: false,
}
@@ -69,6 +91,9 @@ func (sc *SharedContext) Display() *wlclient.Display {
func (sc *SharedContext) Post(fn func()) {
select {
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:
}
}
@@ -89,7 +114,14 @@ func (sc *SharedContext) eventDispatcher() {
}
}
}()
ctx := sc.display.Context()
wlFd := ctx.Fd()
pollFds := []unix.PollFd{
{Fd: int32(wlFd), Events: unix.POLLIN},
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
}
for {
select {
@@ -100,20 +132,33 @@ func (sc *SharedContext) eventDispatcher() {
sc.drainCmdQueue()
if err := ctx.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
log.Errorf("Failed to set read deadline: %v", err)
}
err := ctx.Dispatch()
if err := ctx.SetReadDeadline(time.Time{}); err != nil {
log.Errorf("Failed to clear read deadline: %v", err)
n, err := unix.Poll(pollFds, 50)
if err != nil {
if err == unix.EINTR {
continue
}
log.Errorf("Poll error: %v", err)
return
}
switch {
case err == nil:
case errors.Is(err, os.ErrDeadlineExceeded):
default:
log.Errorf("Wayland connection error: %v", err)
return
if n == 0 {
continue
}
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)
return
}
}
}
}
}
@@ -133,6 +178,9 @@ func (sc *SharedContext) Close() {
close(sc.stopChan)
sc.wg.Wait()
unix.Close(sc.wakeR)
unix.Close(sc.wakeW)
if sc.display != nil {
sc.display.Context().Close()
}

View File

@@ -125,21 +125,47 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
return
}
statusChan := make(chan error, 1)
responded := false
config.SetSucceededHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1SucceededEvent) {
if responded {
return
}
responded = true
log.Info("WlrOutput: configuration succeeded")
statusChan <- nil
config.Destroy()
resultChan <- nil
})
config.SetFailedHandler(func(e wlr_output_management.ZwlrOutputConfigurationV1FailedEvent) {
if responded {
return
}
responded = true
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) {
if responded {
return
}
responded = true
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)
@@ -241,6 +267,7 @@ func (m *Manager) ApplyConfiguration(heads []HeadConfig, test bool) error {
}
if applyErr != nil {
responded = true
config.Destroy()
action := "apply"
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)
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

View File

@@ -145,6 +145,7 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
log.Debugf("WlrOutput: Head %d name: %s", headID, e.Name)
head.name = e.Name
head.ready = true
m.post(func() {
m.updateState()
})
@@ -251,11 +252,11 @@ func (m *Manager) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEven
m.heads.Delete(headID)
m.post(func() {
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.post(func() {
m.updateState()
})
})
@@ -310,11 +311,11 @@ func (m *Manager) handleMode(headID uint32, e wlr_output_management.ZwlrOutputHe
m.modes.Delete(modeID)
m.post(func() {
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.wlMutex.Lock()
handle.Release()
m.wlMutex.Unlock()
m.post(func() {
m.updateState()
})
})
@@ -328,6 +329,10 @@ func (m *Manager) updateState() {
return true
}
if !head.ready {
return true
}
modes := make([]OutputMode, 0)
var currentMode *OutputMode

View File

@@ -90,6 +90,7 @@ type headState struct {
modeIDs []uint32
adaptiveSync uint32
finished bool
ready bool
}
type modeState struct {

View File

@@ -58,6 +58,18 @@ func (ctx *Context) SetReadDeadline(t time.Time) error {
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
// respective wayland protocol.
// Dispatch must be called on the same goroutine as other interactions with the Context.

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
* Major stable release v1.0.0

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
* Bug fixes and improvements
* Rebuild to fix repository metadata issues
-- 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

View File

@@ -1,7 +1,7 @@
%global debug_package %{nil}
Name: dms-git
Version: 0.6.2+git2147.03073f68
Version: 1.0.2+git2528.d336866f
Release: 1%{?dist}
Epoch: 2
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
%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
- Git snapshot (commit 2147: 03073f68)
* Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2+git-5

View File

@@ -4,7 +4,7 @@
Name: dms
Version: 1.0.2
Release: 1%{?dist}
Release: 7%{?dist}
Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
License: MIT

View File

@@ -1,12 +1,14 @@
#!/bin/bash
# Unified OBS upload script for dms packages
# 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:
# ./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 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
@@ -14,6 +16,8 @@ UPLOAD_DEBIAN=true
UPLOAD_OPENSUSE=true
PACKAGE=""
MESSAGE=""
REBUILD_RELEASE="${REBUILD_RELEASE:-}"
POSITIONAL_ARGS=()
for arg in "$@"; do
case "$arg" in
@@ -25,16 +29,43 @@ for arg in "$@"; do
UPLOAD_DEBIAN=false
UPLOAD_OPENSUSE=true
;;
--rebuild=*)
REBUILD_RELEASE="${arg#*=}"
;;
-r|--rebuild)
REBUILD_NEXT=true
;;
*)
if [[ -z "$PACKAGE" ]]; then
PACKAGE="$arg"
elif [[ -z "$MESSAGE" ]]; then
MESSAGE="$arg"
if [[ -n "${REBUILD_NEXT:-}" ]]; then
REBUILD_RELEASE="$arg"
REBUILD_NEXT=false
else
POSITIONAL_ARGS+=("$arg")
fi
;;
esac
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="$HOME/.cache/osc-checkouts"
AVAILABLE_PACKAGES=(dms dms-git)
@@ -70,6 +101,51 @@ if [[ ! -d "distro/debian" ]]; then
echo "Error: Run this script from the repository root"
exit 1
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
if [[ "$PACKAGE" == "all" ]]; then
@@ -145,9 +221,9 @@ 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
elif [[ -n "${FORCE_UPLOAD:-}" ]] && [[ "${FORCE_UPLOAD}" == "true" ]]; then
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
IS_MANUAL=true
echo "==> Local/manual run detected (not in CI)"
@@ -183,12 +259,61 @@ fi
CHANGELOG_VERSION=""
if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
# Format: 0.6.2+git{COMMIT_COUNT}.{COMMIT_HASH} (e.g., 0.6.2+git2256.9162e314)
CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" "distro/debian/$PACKAGE/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "")
if [[ -n "$CHANGELOG_VERSION" ]] && [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
SOURCE_FORMAT_CHECK=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
if [[ "$SOURCE_FORMAT_CHECK" == *"native"* ]]; then
CHANGELOG_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/-[0-9]*$//')
# 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 "")
if [[ -n "$CHANGELOG_VERSION" ]] && [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
SOURCE_FORMAT_CHECK=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
if [[ "$SOURCE_FORMAT_CHECK" == *"native"* ]]; then
CHANGELOG_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/-[0-9]*$//')
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
@@ -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)
if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then
if [[ "$OLD_RELEASE" =~ ^([0-9]+) ]]; then
BASE_RELEASE="${BASH_REMATCH[1]}"
if [[ "$IS_MANUAL" == true ]]; then
NEXT_RELEASE=$((BASE_RELEASE + 1))
echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)"
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${NEXT_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
if [[ "$IS_MANUAL" == true ]] && [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
# Only error for true local manual runs, not CI/workflow runs
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
echo " 🔄 Using manual rebuild release number: $REBUILD_RELEASE"
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${REBUILD_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
cp "$WORK_DIR/$PACKAGE.spec" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec"
else
echo " - Detected same version $NEW_VERSION (release $OLD_RELEASE). Not a manual run, skipping update."
# For automated runs with no version change, we should stop here to avoid unnecessary rebuilds
# However, we need to check if we are also updating Debian, or if this script is expected to continue.
# If this is OpenSUSE only run, we can exit.
if [[ "$UPLOAD_DEBIAN" == false ]]; then
echo "✅ No changes needed for OpenSUSE (not manual). Exiting."
exit 0
fi
echo " - Error: Same version detected ($NEW_VERSION) but no rebuild number specified"
echo " To rebuild, explicitly specify a rebuild number:"
echo " ./distro/scripts/obs-upload.sh opensuse $PACKAGE 2"
echo " or use flag syntax:"
echo " ./distro/scripts/obs-upload.sh opensuse $PACKAGE --rebuild=2"
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
fi
else
echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)"
cp "$WORK_DIR/$PACKAGE.spec" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec"
fi
else
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"
fi
# Copy and update OpenSUSE spec file with the correct version (for -git packages)
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
if [[ "$UPLOAD_DEBIAN" == true ]]; then
echo " Copying debian/ directory into source"
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
# debian/rules uses UPSTREAM_VERSION which is the full version from changelog
if [[ "$PACKAGE" == "dms" ]]; then
@@ -636,292 +798,50 @@ fi
cd "$WORK_DIR"
echo "==> Updating working copy"
if ! osc up; then
echo "Error: Failed to update working copy"
exit 1
fi
# Only auto-increment on manual runs (REBUILD_RELEASE set or not in CI), not automated workflows
OLD_DSC_FILE=""
if [[ -f "$WORK_DIR/.osc/sources/$PACKAGE.dsc" ]]; then
OLD_DSC_FILE="$WORK_DIR/.osc/sources/$PACKAGE.dsc"
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)"
# Server-side cleanup via API
echo "==> Cleaning old tarballs from OBS server (prevents downloading 100+ old versions)"
OBS_FILES=$(osc api "/source/$OBS_PROJECT/$PACKAGE" 2>/dev/null || echo "")
if [[ -n "$OBS_FILES" ]]; then
DELETED_COUNT=0
KEEP_PATTERN=""
if [[ -n "$CHANGELOG_VERSION" ]]; then
BASE_KEEP_VERSION=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
KEEP_PATTERN="${PACKAGE}_${BASE_KEEP_VERSION}"
echo " Keeping tarballs matching: ${KEEP_PATTERN}*"
fi
CHANGELOG_BASE=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
OLD_DSC_BASE=$(echo "$OLD_DSC_VERSION" | sed 's/ppa[0-9]*$//')
if [[ -n "$OLD_DSC_VERSION" ]] && [[ "$OLD_DSC_BASE" == "$CHANGELOG_BASE" ]]; then
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
if [[ -z "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR/debian" ]]; then
echo " Error: Source directory with debian/ not found for version increment"
exit 1
fi
SOURCE_CHANGELOG="$SOURCE_DIR/debian/changelog"
if [[ ! -f "$SOURCE_CHANGELOG" ]]; then
echo " Error: Changelog not found in source directory: $SOURCE_CHANGELOG"
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
done
if [[ "$PACKAGE" == "dms" ]] && [[ -f "$WORK_DIR/dms-source.tar.gz" ]]; then
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
echo " Warning: Source directory $CURRENT_DIR not found, extracting from existing tarball"
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
} >"$TEMP_CHANGELOG"
cp "$TEMP_CHANGELOG" "$EXPECTED_DIR/debian/changelog"
rm -f "$TEMP_CHANGELOG"
fi
fi
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
cd "$REPO_ROOT"
else
echo " Error: Could not extract or find source directory"
rm -rf "$EXTRACT_DIR"
exit 1
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
for old_file in $(echo "$OBS_FILES" | grep -oP '(?<=name=")[^"]*\.(tar\.gz|tar\.xz|tar\.bz2)(?=")' || true); do
if [[ -n "$KEEP_PATTERN" ]] && [[ "$old_file" == ${KEEP_PATTERN}* ]]; then
echo " - Keeping current version: $old_file"
continue
fi
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
echo " - Keeping source tarball: $old_file"
continue
fi
echo " - Deleting from server: $old_file"
if osc api -X DELETE "/source/$OBS_PROJECT/$PACKAGE/$old_file" 2>/dev/null; then
((DELETED_COUNT++)) || true
fi
done
if [[ $DELETED_COUNT -gt 0 ]]; then
echo " ✓ Deleted $DELETED_COUNT old tarball(s) from server"
else
echo " ✓ No old tarballs found on server (current version preserved)"
fi
else
echo " ⚠️ Could not fetch file list from server, skipping cleanup"
fi
# Fallback update with --server-side-source-service-files flag only syncs metadata (spec, dsc, _service)
echo "==> Updating working copy"
if ! osc up --server-side-source-service-files 2>/dev/null; then
echo " Note: Using regular update (--server-side-source-service-files not supported)"
if ! osc up; then
echo "Error: Failed to update working copy"
exit 1
fi
fi

View File

@@ -36,7 +36,6 @@ fi
PACKAGE_DIR="$1"
UBUNTU_SERIES="${2:-noble}"
# Validate package directory
if [ ! -d "$PACKAGE_DIR" ]; then
error "Package directory not found: $PACKAGE_DIR"
exit 1
@@ -47,21 +46,43 @@ if [ ! -d "$PACKAGE_DIR/debian" ]; then
exit 1
fi
# Get absolute path
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
PACKAGE_PARENT=$(dirname "$PACKAGE_DIR")
# Create temporary working directory (like OBS)
TEMP_WORK_DIR=$(mktemp -d -t ppa_build_work_XXXXXX)
trap 'rm -rf "$TEMP_WORK_DIR"' EXIT
# Choose temp directory: use /tmp in CI, ~/tmp locally (keeps artifacts out of repo)
if [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${CI:-}" ]]; then
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 "Package directory: $PACKAGE_DIR"
info "Working directory: $TEMP_WORK_DIR"
info "Target Ubuntu series: $UBUNTU_SERIES"
# Check for required files
REQUIRED_FILES=(
"debian/control"
"debian/rules"
@@ -87,14 +108,64 @@ fi
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
error "debuild not found. Install devscripts:"
error " sudo dnf install devscripts"
exit 1
fi
# Extract package info from changelog
cd "$PACKAGE_DIR"
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
SOURCE_NAME=$(dpkg-parsechangelog -S Source)
@@ -102,41 +173,24 @@ SOURCE_NAME=$(dpkg-parsechangelog -S Source)
info "Source package: $SOURCE_NAME"
info "Version: $CHANGELOG_VERSION"
# Check if version targets correct Ubuntu series
CHANGELOG_SERIES=$(dpkg-parsechangelog -S Distribution)
if [ "$CHANGELOG_SERIES" != "$UBUNTU_SERIES" ] && [ "$CHANGELOG_SERIES" != "UNRELEASED" ]; then
warn "Changelog targets '$CHANGELOG_SERIES' but building for '$UBUNTU_SERIES'"
warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES"
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..."
cp -r "$PACKAGE_DIR" "$TEMP_WORK_DIR/"
WORK_PACKAGE_DIR="$TEMP_WORK_DIR/$PACKAGE_NAME"
# Detect package type and update version automatically
cd "$WORK_PACKAGE_DIR"
if [ -f "$WORK_PACKAGE_DIR/debian/files" ]; then
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() {
local repo="$1"
# Try GitHub API first (faster)
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)
if [ -n "$LATEST_TAG" ]; then
@@ -144,8 +198,7 @@ get_latest_tag() {
return
fi
fi
# Fallback: clone and get latest tag
TEMP_REPO=$(mktemp -d)
TEMP_REPO=$(mktemp -d "$TEMP_BASE/ppa_tag_XXXXXX")
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 "")
rm -rf "$TEMP_REPO"
@@ -153,27 +206,21 @@ get_latest_tag() {
fi
}
# Detect if package is git-based
IS_GIT_PACKAGE=false
GIT_REPO=""
SOURCE_DIR=""
# Check package name for -git suffix
if [[ "$PACKAGE_NAME" == *"-git" ]]; then
IS_GIT_PACKAGE=true
fi
# Check rules file for git clone patterns and extract repo
if grep -q "git clone" debian/rules 2>/dev/null; then
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 "")
if [ -n "$GIT_URL" ]; then
GIT_REPO="$GIT_URL"
fi
fi
# Special handling for known packages
case "$PACKAGE_NAME" in
dms-git)
IS_GIT_PACKAGE=true
@@ -182,65 +229,141 @@ dms-git)
;;
dms)
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)
# 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]*$//')
# Download amd64 binary (will be included in source package)
if [ ! -f "dms-distropkg-amd64.gz" ]; then
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
success "amd64 binary downloaded"
else
error "Failed to download dms-distropkg-amd64.gz"
exit 1
fi
fi
# Download source tarball for QML files
if [ ! -f "dms-source.tar.gz" ]; then
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
success "source tarball downloaded"
else
error "Failed to download dms-source.tar.gz"
exit 1
fi
fi
;;
dms-greeter)
GIT_REPO="AvengeMedia/DankMaterialShell"
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
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
success "source tarball downloaded"
else
error "Failed to download dms-greeter-source.tar.gz"
exit 1
fi
fi
;;
danksearch)
# danksearch uses pre-built binary from releases
GIT_REPO="AvengeMedia/danksearch"
;;
dgop)
# dgop uses pre-built binary from releases
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]*$//')
case "$PACKAGE_NAME" in
dms)
info "Downloading pre-built binaries and source for dms..."
if [ ! -f "dms-distropkg-amd64.gz" ]; then
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
success "amd64 binary downloaded"
else
error "Failed to download dms-distropkg-amd64.gz"
exit 1
fi
fi
if [ ! -f "dms-source.tar.gz" ]; then
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
success "source tarball downloaded"
else
error "Failed to download dms-source.tar.gz"
exit 1
fi
fi
;;
dms-greeter)
info "Downloading source for dms-greeter..."
if [ ! -f "dms-greeter-source.tar.gz" ]; then
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
success "source tarball downloaded"
else
error "Failed to download dms-greeter-source.tar.gz"
exit 1
fi
fi
;;
esac
fi
# Handle git packages
if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
info "Detected git package: $PACKAGE_NAME"
# Determine source directory name
if [ -z "$SOURCE_DIR" ]; then
# Default: use package name without -git suffix + -source or -repo
BASE_NAME=$(echo "$PACKAGE_NAME" | sed 's/-git$//')
if [ -d "${BASE_NAME}-source" ] 2>/dev/null; then
SOURCE_DIR="${BASE_NAME}-source"
@@ -253,27 +376,18 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
fi
fi
# Always clone fresh source to get 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
# Get git commit info from fresh clone
GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short 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)
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)
fi
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")
fi
# Verify we got valid commit info
if [ -z "$GIT_COMMIT_COUNT" ] || [ "$GIT_COMMIT_COUNT" = "0" ]; then
error "Failed to get commit count from $GIT_REPO"
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"
# Update changelog with git commit info
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)
# Build base version (without ppa suffix yet)
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
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
PPA_NUM=$REBUILD_RELEASE
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
else
PPA_NUM=1
# 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
# 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
info "New commit or first build, using PPA number $PPA_NUM"
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
PPA_NUM=$REBUILD_RELEASE
info "Using REBUILD_RELEASE=$REBUILD_RELEASE for PPA number"
else
PPA_NUM=1
info "Using PPA number $PPA_NUM"
fi
fi
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)
# Get current changelog content - find the next package header line (starts with package name)
# Skip the first entry entirely by finding the second occurrence of the package name at start of line
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
# Single changelog entry (git snapshots don't need history)
cat >debian/changelog <<EOF
${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* Git snapshot (commit ${GIT_COMMIT_COUNT}: ${GIT_COMMIT_HASH})
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
# 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
-- 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)
# Now clone to source directory (without .git for inclusion in package)
rm -rf "$SOURCE_DIR"
cp -r "$TEMP_CLONE" "$SOURCE_DIR"
# Save version info for dms-git build process
if [ "$PACKAGE_NAME" = "dms-git" ]; then
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 "COMMIT=${GIT_COMMIT_HASH}" >>"$SOURCE_DIR/.dms-version"
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..."
cd "$SOURCE_DIR/core"
# Create vendor directory with all dependencies
go mod vendor
if [ ! -d "vendor" ]; then
@@ -378,167 +495,21 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
rm -rf "$SOURCE_DIR/.git"
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"
else
error "Failed to clone $GIT_REPO"
rm -rf "$TEMP_CLONE"
exit 1
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
# Handle packages that need pre-built binaries downloaded
cd "$PACKAGE_DIR"
cd "$WORK_PACKAGE_DIR"
case "$PACKAGE_NAME" in
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]*$//')
# 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
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
@@ -564,46 +535,26 @@ danksearch)
fi
;;
dgop)
# dgop binary should already be committed in the repo
if [ ! -f "dgop" ]; then
warn "dgop binary not found - should be committed to repo"
fi
;;
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..."
echo
# Determine if we need to include orig tarball (-sa) or just debian changes (-sd)
# Check if .orig.tar.xz already exists in real parent directory (previous build)
ORIG_TARBALL="${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz"
if [ -f "$PACKAGE_PARENT/$ORIG_TARBALL" ]; then
SOURCE_FORMAT=$(head -1 "$WORK_PACKAGE_DIR/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
# Native format packages don't use orig tarballs - they include everything in one tarball
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)"
# Copy it to temp parent so debuild can find it
cp "$PACKAGE_PARENT/$ORIG_TARBALL" "$TEMP_WORK_DIR/"
DEBUILD_SOURCE_FLAG="-sd"
else
@@ -611,20 +562,20 @@ else
DEBUILD_SOURCE_FLAG="-sa"
fi
# Use -S for source only, -sa/-sd for source inclusion
# -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
echo
success "Source package built successfully!"
# Copy build artifacts back to parent directory
info "Copying build artifacts to $PACKAGE_PARENT..."
cp -v "$TEMP_WORK_DIR"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* "$PACKAGE_PARENT/" 2>/dev/null || true
TEMP_MARKER_FILE="$PACKAGE_PARENT/.ppa_build_temp_${PACKAGE_NAME}"
echo "PPA_BUILD_TEMP_DIR=$TEMP_WORK_DIR" > "$TEMP_MARKER_FILE"
# List generated files
info "Generated files in $PACKAGE_PARENT:"
ls -lh "$PACKAGE_PARENT"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* 2>/dev/null || true
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:"
ls -lh "$PACKAGE_PARENT"/"${SOURCE_NAME}"_"${CHANGELOG_VERSION}"* 2>/dev/null || true
fi
# Show what to do next
echo

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
distro/scripts/ppa-status.sh Executable file
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 ""

View File

@@ -1,10 +1,16 @@
#!/bin/bash
# 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:
# ./create-and-upload.sh ../dms dms questing
# ./create-and-upload.sh ../danklinux/dgop danklinux questing --keep-builds
# Examples:
# ./ppa-upload.sh dms # Single package (auto-detects PPA)
# ./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
@@ -19,72 +25,204 @@ success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Parse arguments
AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
KEEP_BUILDS=false
ARGS=()
REBUILD_RELEASE=""
POSITIONAL_ARGS=()
for arg in "$@"; do
if [ "$arg" = "--keep-builds" ]; then
KEEP_BUILDS=true
else
ARGS+=("$arg")
fi
case "$arg" in
--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
POSITIONAL_ARGS+=("$arg")
fi
;;
esac
done
if [ ${#ARGS[@]} -lt 2 ]; then
error "Usage: $0 <package-dir> <ppa-name> [ubuntu-series] [--keep-builds]"
echo
echo "Arguments:"
echo " package-dir : Path to package directory (e.g., ../dms, ../danklinux/dgop)"
echo " ppa-name : PPA name (danklinux, dms, dms-git)"
echo " ubuntu-series : Ubuntu series (optional, default: questing)"
echo " Supported: questing (25.10) and newer only"
echo " Note: Requires Qt 6.6+ (quickshell requirement)"
echo " --keep-builds : Keep build artifacts after upload (optional)"
echo
echo "Examples:"
echo " $0 ../dms dms questing"
echo " $0 ../danklinux/dgop danklinux questing --keep-builds"
echo " $0 ../dms-git dms-git # Defaults to questing"
exit 1
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
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}")
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
fi
fi
PACKAGE_DIR="${ARGS[0]}"
PPA_NAME="${ARGS[1]}"
UBUNTU_SERIES="${ARGS[2]:-questing}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
UPLOAD_SCRIPT="$SCRIPT_DIR/ppa-dput.sh"
# Validate scripts exist
if [ ! -f "$BUILD_SCRIPT" ]; then
error "Build script not found: $BUILD_SCRIPT"
exit 1
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_NAME=$(basename "$PACKAGE_DIR")
PARENT_DIR=$(dirname "$PACKAGE_DIR")
info "Building and uploading: $PACKAGE_NAME"
info "Package directory: $PACKAGE_DIR"
info "PPA: ppa:avengemedia/$PPA_NAME"
info "Ubuntu series: $UBUNTU_SERIES"
if [[ -n "$REBUILD_RELEASE" ]]; then
info "Rebuild release number: ppa$REBUILD_RELEASE"
fi
echo
# Step 1: Build 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
error "Build failed!"
exit 1
fi
# Find the changes file
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
TEMP_DIR_FILE="$PARENT_DIR/.ppa_build_temp_${PACKAGE_NAME}"
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
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."
exit 0
fi
@@ -92,14 +230,11 @@ fi
info "Found changes file: $CHANGES_FILE"
echo
# Step 2: Upload 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
warn "Using lftp for upload"
# Find all files to upload
BUILD_DIR=$(dirname "$CHANGES_FILE")
CHANGES_BASENAME=$(basename "$CHANGES_FILE")
DSC_FILE="${CHANGES_BASENAME/_source.changes/.dsc}"
@@ -127,7 +262,6 @@ if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "
info " - $BUILDINFO"
echo
# lftp build dir change
LFTP_SCRIPT=$(mktemp)
cat >"$LFTP_SCRIPT" <<EOF
cd ~avengemedia/ubuntu/$PPA_NAME/
@@ -148,17 +282,11 @@ EOF
exit 1
fi
else
# Use dput for other PPAs
if [ ! -f "$UPLOAD_SCRIPT" ]; then
error "Upload script not found: $UPLOAD_SCRIPT"
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
# This branch should not be reached for DMS packages
# All DMS packages (dms, dms-git, dms-greeter) use lftp
error "Unknown PPA: $PPA_NAME"
error "DMS packages use lftp for upload. Supported PPAs: dms, dms-git, danklinux"
exit 1
fi
echo
@@ -167,11 +295,17 @@ info "Monitor build progress at:"
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
echo
# Step 3: Cleanup (unless --keep-builds is specified)
if [ "$KEEP_BUILDS" = "false" ]; then
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=(
"${PACKAGE_NAME}_*.dsc"
"${PACKAGE_NAME}_*.tar.xz"
@@ -191,7 +325,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
done
done
# Clean up downloaded binaries in package directory
case "$PACKAGE_NAME" in
danksearch)
if [ -f "$PACKAGE_DIR/dsearch-amd64" ]; then
@@ -204,7 +337,6 @@ if [ "$KEEP_BUILDS" = "false" ]; then
fi
;;
dms)
# Remove downloaded binaries and source
if [ -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" ]; then
rm -f "$PACKAGE_DIR/dms-distropkg-amd64.gz"
REMOVED=$((REMOVED + 1))

View File

@@ -1 +0,0 @@
danksearch_0.0.7ppa3_source.buildinfo utils optional

View File

@@ -1 +0,0 @@
dgop_0.1.11ppa2_source.buildinfo utils optional

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
* Previous updates included in build
* Git snapshot (commit 2531: 208266df)
-- 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

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

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

View File

@@ -1 +0,0 @@
dms-greeter_0.6.2ppa3_source.buildinfo x11 optional

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

View File

@@ -1 +0,0 @@
dms_1.0.0ppa4_source.buildinfo x11 optional

View File

@@ -44,6 +44,10 @@
pkgs: 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 =
pkgs: with pkgs.kdePackages; [
kirigami.unwrapped
@@ -78,7 +82,7 @@
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-yqV12LssYV0zuUPLjTzJE0e49uUER95dRH4LTcRJeGc=";
vendorHash = "sha256-DINaA5LCOWoxBIewuc39Rnwj6NdZoET7Q++B11Qg5rI=";
subPackages = [ "cmd/dms" ];
@@ -108,7 +112,8 @@
wrapProgram $out/bin/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 \
$out/lib/systemd/user/dms.service
@@ -174,6 +179,10 @@
prek
uv # for prek
# Nix development tools
nixd
nil
]
++ devQmlPkgs;
@@ -183,6 +192,7 @@
'';
QML2_IMPORT_PATH = mkQmlImportPath pkgs devQmlPkgs;
QT_PLUGIN_PATH = mkQtPluginPath pkgs devQmlPkgs;
};
}
);

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton {
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"
@@ -352,6 +352,7 @@ Singleton {
property string customPowerActionReboot: ""
property string customPowerActionPowerOff: ""
property bool updaterHideWidget: false
property bool updaterUseCustomCommand: false
property string updaterCustomCommand: ""
property string updaterTerminalAdditionalParams: ""
@@ -396,7 +397,10 @@ Singleton {
visible: true,
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true
maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace"
}
]

View File

@@ -203,10 +203,6 @@ Singleton {
"value": "scheme-vibrant",
"label": "Vibrant",
"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",
"label": "Content",

View File

@@ -1,5 +1,4 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
@@ -17,40 +16,67 @@ Singleton {
pciId: "",
mountPath: "/",
minimumWidth: true,
showSwap: false
}
leftModel.append(dummy)
centerModel.append(dummy)
rightModel.append(dummy)
showSwap: false,
mediaSize: 1,
showNetworkIcon: true,
showBluetoothIcon: true,
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(centerModel, center)
update(rightModel, right)
update(leftModel, left);
update(centerModel, center);
update(rightModel, right);
}
function update(model, order) {
model.clear()
model.clear();
for (var i = 0; i < order.length; i++) {
var widgetId = typeof order[i] === "string" ? order[i] : order[i].id
var enabled = typeof order[i] === "string" ? true : order[i].enabled
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 isObj = typeof order[i] !== "string";
var widgetId = isObj ? order[i].id : order[i];
var item = {
widgetId: widgetId,
enabled: enabled
}
if (size !== undefined) item.size = size
if (selectedGpuIndex !== undefined) item.selectedGpuIndex = selectedGpuIndex
if (pciId !== undefined) item.pciId = pciId
if (mountPath !== undefined) item.mountPath = mountPath
if (minimumWidth !== undefined) item.minimumWidth = minimumWidth
if (showSwap !== undefined) item.showSwap = showSwap
enabled: isObj ? order[i].enabled : true
};
if (isObj && order[i].size !== undefined)
item.size = order[i].size;
if (isObj && order[i].selectedGpuIndex !== undefined)
item.selectedGpuIndex = order[i].selectedGpuIndex;
if (isObj && order[i].pciId !== undefined)
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);
}
}
}

View File

@@ -250,6 +250,7 @@ var SPEC = {
customPowerActionReboot: { def: "" },
customPowerActionPowerOff: { def: "" },
updaterHideWidget: { def: false },
updaterUseCustomCommand: { def: false },
updaterCustomCommand: { def: "" },
updaterTerminalAdditionalParams: { def: "" },
@@ -293,7 +294,10 @@ var SPEC = {
visible: true,
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true
maximizeDetection: true,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace"
}], onChange: "updateBarConfigs" }
};

View File

@@ -113,6 +113,12 @@ function migrateToVersion(obj, targetVersion) {
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;
}

View File

@@ -49,12 +49,14 @@ Item {
readonly property alias backgroundWindow: backgroundWindow
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useSingleWindow: root.useHyprlandFocusGrab
signal opened
signal dialogClosed
signal backgroundClicked
property bool animationsEnabled: true
readonly property bool useBackgroundWindow: true
readonly property bool useBackgroundWindow: !useSingleWindow
function open() {
ModalManager.openModal(root);
@@ -205,7 +207,7 @@ Item {
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
enabled: root.useBackgroundWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const clickX = mouse.x;
const clickY = mouse.y;
@@ -222,7 +224,7 @@ Item {
anchors.fill: parent
color: "black"
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 {
enabled: root.animationsEnabled
@@ -271,15 +273,19 @@ Item {
anchors {
left: true
top: true
right: root.useSingleWindow
bottom: root.useSingleWindow
}
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
right: 0
bottom: 0
}
implicitWidth: root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.alignedHeight + (shadowBuffer * 2)
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
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 {
id: modalContainer
x: shadowBuffer
y: shadowBuffer
x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: root.useSingleWindow ? root.alignedY : shadowBuffer
width: root.alignedWidth
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 real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset

View File

@@ -7,10 +7,11 @@ DankModal {
id: root
property string outputName: ""
property var position: undefined
property var mode: undefined
property var vrr: undefined
property int countdown: 15
property var changes: []
property int countdown: 10
signal confirmed
signal reverted
shouldBeVisible: false
allowStacking: true
@@ -23,23 +24,27 @@ DankModal {
repeat: true
running: root.shouldBeVisible
onTriggered: {
countdown--;
if (countdown <= 0) {
revert();
root.countdown--
if (root.countdown <= 0) {
root.reverted()
root.close()
}
}
}
onOpened: {
countdown = 15;
countdownTimer.start();
countdown = 10
countdownTimer.start()
}
onClosed: {
countdownTimer.stop();
onDialogClosed: {
countdownTimer.stop()
}
onBackgroundClicked: revert
onBackgroundClicked: {
root.reverted()
root.close()
}
content: Component {
FocusScope {
@@ -50,13 +55,15 @@ DankModal {
implicitHeight: mainColumn.implicitHeight
Keys.onEscapePressed: event => {
revert();
event.accepted = true;
root.reverted()
root.close()
event.accepted = true
}
Keys.onReturnPressed: event => {
confirm();
event.accepted = true;
root.confirmed()
root.close()
event.accepted = true
}
Column {
@@ -69,81 +76,42 @@ DankModal {
anchors.topMargin: Theme.spacingM
spacing: Theme.spacingM
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Confirm Display Changes")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Display settings for ") + outputName
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
}
StyledText {
text: I18n.tr("Confirm Display Changes")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width
height: 80
height: 70
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
Column {
StyledText {
anchors.centerIn: parent
spacing: 4
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
color: Theme.primary
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
text: root.countdown + "s"
font.pixelSize: Theme.fontSizeXLarge * 1.5
color: Theme.primary
font.weight: Font.Bold
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: root.changes.length > 0
StyledText {
text: I18n.tr("Changes:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
Repeater {
model: root.changes
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
StyledText {
required property var modelData
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
@@ -180,7 +148,10 @@ DankModal {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: revert
onClicked: {
root.reverted()
root.close()
}
}
}
@@ -206,7 +177,10 @@ DankModal {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: confirm
onClicked: {
root.confirmed()
root.close()
}
}
Behavior on color {
@@ -228,18 +202,11 @@ DankModal {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: revert
onClicked: {
root.reverted()
root.close()
}
}
}
}
function confirm() {
displaysTab.confirmChanges();
close();
}
function revert() {
displaysTab.revertChanges();
close();
}
}

View File

@@ -98,6 +98,12 @@ DankModal {
return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
}
function toggleDoNotDisturb(): string {
SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
return "NOTIFICATION_MODAL_TOGGLE_DND_SUCCESS";
}
target: "notifications"
}

View File

@@ -125,13 +125,45 @@ FocusScope {
}
Loader {
id: displaysLoader
id: displayConfigLoader
anchors.fill: parent
active: root.currentIndex === 6
active: root.currentIndex === 24
visible: 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: {
if (active && item) {

View File

@@ -58,7 +58,7 @@ FloatingWindow {
objectName: "settingsModal"
title: I18n.tr("Settings", "settings window title")
minimumSize: Qt.size(500, 400)
implicitWidth: 800
implicitWidth: 900
implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
color: Theme.surfaceContainer
visible: false

View File

@@ -144,6 +144,32 @@ Rectangle {
"tabIndex": 2,
"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",
"text": I18n.tr("Network"),
@@ -157,12 +183,6 @@ Rectangle {
"icon": "computer",
"collapsedByDefault": true,
"children": [
{
"id": "displays",
"text": I18n.tr("Displays"),
"icon": "monitor",
"tabIndex": 6
},
{
"id": "printers",
"text": I18n.tr("Printers"),

View File

@@ -27,7 +27,7 @@ PluginComponent {
ccDetailContent: Component {
VpnDetailContent {
listHeight: 180
listHeight: 260
}
}
}

View File

@@ -18,13 +18,16 @@ Item {
function getDetailHeight(section) {
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);
if (section === "bluetooth")
return Math.min(350, maxAvailable);
if (section.startsWith("brightnessSlider_"))
case section.startsWith("brightnessSlider_"):
return Math.min(400, maxAvailable);
return Math.min(250, maxAvailable);
default:
return Math.min(250, maxAvailable);
}
}
Loader {

View File

@@ -594,7 +594,8 @@ PanelWindow {
propagateComposedEvents: true
z: -1
property real scrollAccumulator: 0
property real scrollAccumulatorY: 0
property real scrollAccumulatorX: 0
property real touchpadThreshold: 500
property bool actionInProgress: false
@@ -604,7 +605,30 @@ PanelWindow {
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 => {
if (!(barConfig?.scrollEnabled ?? true)) {
wheel.accepted = false;
return;
}
if (actionInProgress) {
wheel.accepted = false;
return;
@@ -612,9 +636,34 @@ PanelWindow {
const deltaY = wheel.angleDelta.y;
const deltaX = wheel.angleDelta.x;
const xBehavior = barConfig?.scrollXBehavior ?? "column";
const yBehavior = barConfig?.scrollYBehavior ?? "workspace";
if (CompositorService.isNiri && Math.abs(deltaX) > Math.abs(deltaY)) {
topBarContent.switchApp(deltaX);
if (CompositorService.isNiri && xBehavior !== "none" && Math.abs(deltaX) > Math.abs(deltaY)) {
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;
return;
}
@@ -623,19 +672,20 @@ PanelWindow {
const direction = deltaY < 0 ? 1 : -1;
if (isMouseWheel) {
topBarContent.switchWorkspace(direction);
actionInProgress = true;
cooldownTimer.restart();
} else {
scrollAccumulator += deltaY;
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
const touchDirection = scrollAccumulator < 0 ? 1 : -1;
topBarContent.switchWorkspace(touchDirection);
scrollAccumulator = 0;
if (handleScrollAction(yBehavior, direction)) {
actionInProgress = true;
cooldownTimer.restart();
}
} else {
scrollAccumulatorY += deltaY;
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
const touchDirection = scrollAccumulatorY < 0 ? 1 : -1;
if (handleScrollAction(yBehavior, touchDirection)) {
actionInProgress = true;
cooldownTimer.restart();
}
scrollAccumulatorY = 0;
}
}
wheel.accepted = false;

View File

@@ -552,22 +552,31 @@ DankPopout {
}
}
DankButtonGroup {
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined")
return 1;
return profileModel.findIndex(profile => root.isActiveProfile(profile));
}
Item {
width: parent.width
height: profileButtonGroup.height * profileButtonGroup.scale
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
currentIndex: currentProfileIndex
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected)
return;
root.setProfile(profileModel[index]);
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 int currentProfileIndex: {
if (typeof PowerProfiles === "undefined")
return 1;
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))
currentIndex: currentProfileIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
root.setProfile(profileModel[index]);
}
}
}

View File

@@ -15,6 +15,7 @@ DankPopout {
triggerY = y;
triggerWidth = width;
triggerSection = section;
triggerScreen = screen;
root.screen = screen;
storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
@@ -102,6 +103,8 @@ DankPopout {
screen: triggerScreen
shouldBeVisible: false
onBackgroundClicked: close()
content: Component {
Rectangle {
id: layoutContent

View File

@@ -13,14 +13,14 @@ BasePill {
property var widgetData: null
property string screenName: ""
property string screenModel: ""
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
property bool showVpnIcon: SettingsData.controlCenterShowVpnIcon
property bool showBrightnessIcon: SettingsData.controlCenterShowBrightnessIcon
property bool showMicIcon: SettingsData.controlCenterShowMicIcon
property bool showBatteryIcon: SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: SettingsData.controlCenterShowPrinterIcon
property bool showNetworkIcon: widgetData?.showNetworkIcon !== undefined ? widgetData.showNetworkIcon : SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: widgetData?.showBluetoothIcon !== undefined ? widgetData.showBluetoothIcon : SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: widgetData?.showAudioIcon !== undefined ? widgetData.showAudioIcon : SettingsData.controlCenterShowAudioIcon
property bool showVpnIcon: widgetData?.showVpnIcon !== undefined ? widgetData.showVpnIcon : SettingsData.controlCenterShowVpnIcon
property bool showBrightnessIcon: widgetData?.showBrightnessIcon !== undefined ? widgetData.showBrightnessIcon : SettingsData.controlCenterShowBrightnessIcon
property bool showMicIcon: widgetData?.showMicIcon !== undefined ? widgetData.showMicIcon : SettingsData.controlCenterShowMicIcon
property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon
Loader {
active: root.showPrinterIcon

View File

@@ -358,7 +358,7 @@ Item {
IconImage {
id: iconImg
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
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
@@ -385,7 +385,7 @@ Item {
DankIcon {
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
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
@@ -607,7 +607,7 @@ Item {
IconImage {
id: iconImg
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
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
@@ -634,7 +634,7 @@ Item {
DankIcon {
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
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"

View File

@@ -11,6 +11,9 @@ BasePill {
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
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 {
service: SystemUpdateService
}

View File

@@ -649,6 +649,16 @@ Item {
anchors.fill: parent
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 => {
if (mouse.button === Qt.RightButton) {
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 {
@@ -808,7 +842,12 @@ Item {
wsData = modelData;
}
delegateRoot.loadedWorkspaceData = wsData;
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
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;
}
var icData = null;
if (wsData?.name) {
@@ -844,8 +883,8 @@ Item {
radius: Theme.cornerRadius
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
border.width: isUrgent && !isActive ? 2 : 0
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
border.width: isUrgent ? 2 : 0
border.color: isUrgent ? Theme.error : Theme.withAlpha(Theme.error, 0)
Behavior on width {
NumberAnimation {

View File

@@ -1,13 +1,11 @@
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Greetd
import Quickshell.Services.Pam
import qs.Common
import qs.Services
import qs.Widgets
@@ -22,74 +20,67 @@ Item {
property string hyprlandCurrentLayout: ""
property string hyprlandKeyboard: ""
property int hyprlandLayoutCount: 0
property bool isPrimaryScreen: {
if (!Qt.application.screens || Qt.application.screens.length === 0)
return true
if (!screenName || screenName === "")
return true
return screenName === Qt.application.screens[0].name
}
property bool isPrimaryScreen: !Quickshell.screens?.length || screenName === Quickshell.screens[0]?.name
signal launchRequested
function pickRandomFact() {
randomFact = Facts.getRandomFact()
randomFact = Facts.getRandomFact();
}
property bool weatherInitialized: false
function initWeatherService() {
if (weatherInitialized)
return
return;
if (!GreetdSettings.settingsLoaded)
return
return;
if (!GreetdSettings.weatherEnabled)
return
weatherInitialized = true
WeatherService.addRef()
WeatherService.forceRefresh()
return;
weatherInitialized = true;
WeatherService.addRef();
WeatherService.forceRefresh();
}
Connections {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded)
initWeatherService()
initWeatherService();
}
}
Component.onCompleted: {
pickRandomFact()
initWeatherService()
pickRandomFact();
initWeatherService();
if (isPrimaryScreen) {
sessionListProc.running = true
applyLastSuccessfulUser()
sessionListProc.running = true;
applyLastSuccessfulUser();
}
if (CompositorService.isHyprland)
updateHyprlandLayout()
updateHyprlandLayout();
}
function applyLastSuccessfulUser() {
const lastUser = GreetdMemory.lastSuccessfulUser
const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser
GreeterState.usernameInput = lastUser
GreeterState.showPasswordInput = true
PortalService.getGreeterUserProfileImage(lastUser)
GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser);
}
}
Component.onDestruction: {
if (weatherInitialized)
WeatherService.removeRef()
WeatherService.removeRef();
}
function updateHyprlandLayout() {
if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true
hyprlandLayoutProcess.running = true;
}
}
@@ -100,27 +91,27 @@ Item {
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
hyprlandKeyboard = mainKeyboard.name
const data = JSON.parse(text);
const mainKeyboard = data.keyboards.find(kb => kb.main === true);
hyprlandKeyboard = mainKeyboard.name;
if (mainKeyboard && mainKeyboard.active_keymap) {
const parts = mainKeyboard.active_keymap.split(" ")
const parts = mainKeyboard.active_keymap.split(" ");
if (parts.length > 0) {
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase()
hyprlandCurrentLayout = parts[0].substring(0, 2).toUpperCase();
} else {
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase()
hyprlandCurrentLayout = mainKeyboard.active_keymap.substring(0, 2).toUpperCase();
}
} else {
hyprlandCurrentLayout = ""
hyprlandCurrentLayout = "";
}
if (mainKeyboard && mainKeyboard.layout_names) {
hyprlandLayoutCount = mainKeyboard.layout_names.length
hyprlandLayoutCount = mainKeyboard.layout_names.length;
} else {
hyprlandLayoutCount = 0
hyprlandLayoutCount = 0;
}
} catch (e) {
hyprlandCurrentLayout = ""
hyprlandLayoutCount = 0
hyprlandCurrentLayout = "";
hyprlandLayoutCount = 0;
}
}
}
@@ -132,7 +123,7 @@ Item {
function onRawEvent(event) {
if (event.name === "activelayout")
updateHyprlandLayout()
updateHyprlandLayout();
}
}
@@ -140,7 +131,7 @@ Item {
target: GreetdMemory
enabled: isPrimaryScreen
function onLastSuccessfulUserChanged() {
applyLastSuccessfulUser()
applyLastSuccessfulUser();
}
}
@@ -148,7 +139,7 @@ Item {
target: GreeterState
function onUsernameChanged() {
if (GreeterState.username) {
PortalService.getGreeterUserProfileImage(GreeterState.username)
PortalService.getGreeterUserProfileImage(GreeterState.username);
}
}
}
@@ -157,10 +148,10 @@ Item {
anchors.fill: parent
screenName: root.screenName
visible: {
var _ = SessionData.perMonitorWallpaper
var __ = SessionData.monitorWallpapers
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#"))
var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return !currentWallpaper || currentWallpaper === "" || (currentWallpaper && currentWallpaper.startsWith("#"));
}
}
@@ -169,10 +160,10 @@ Item {
anchors.fill: parent
source: {
var _ = SessionData.perMonitorWallpaper
var __ = SessionData.monitorWallpapers
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : ""
var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? currentWallpaper : "";
}
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
smooth: true
@@ -213,10 +204,12 @@ Item {
color: "transparent"
Item {
anchors.centerIn: parent
anchors.verticalCenterOffset: -100
id: clockContainer
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.verticalCenter
anchors.bottomMargin: 60
width: parent.width
height: 140
height: clockText.implicitHeight
Row {
id: clockText
@@ -225,10 +218,8 @@ Item {
spacing: 0
property string fullTimeStr: {
const format = GreetdSettings.use24HourClock
? (GreetdSettings.showSeconds ? "HH:mm:ss" : "HH:mm")
: (GreetdSettings.showSeconds ? "h:mm:ss AP" : "h:mm AP")
return systemClock.date.toLocaleTimeString(Qt.locale(), format)
const format = GreetdSettings.use24HourClock ? (GreetdSettings.showSeconds ? "HH:mm:ss" : "HH:mm") : (GreetdSettings.showSeconds ? "h:mm:ss AP" : "h:mm AP");
return systemClock.date.toLocaleTimeString(Qt.locale(), format);
}
property var timeParts: fullTimeStr.split(':')
property string hours: timeParts[0] || ""
@@ -236,8 +227,8 @@ Item {
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
property string ampm: {
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i)
return match ? match[0].trim() : ""
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
return match ? match[0].trim() : "";
}
property bool hasSeconds: timeParts.length > 2
@@ -332,13 +323,15 @@ Item {
}
StyledText {
anchors.centerIn: parent
anchors.verticalCenterOffset: -10
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockContainer.bottom
anchors.topMargin: 4
text: {
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
color: "white"
@@ -346,8 +339,9 @@ Item {
}
Item {
anchors.centerIn: parent
anchors.verticalCenterOffset: 80
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: dateText.bottom
anchors.topMargin: Theme.spacingL
width: 380
height: 140
@@ -364,14 +358,14 @@ Item {
Layout.preferredHeight: 60
imageSource: {
if (PortalService.profileImage === "") {
return ""
return "";
}
if (PortalService.profileImage.startsWith("/")) {
return "file://" + PortalService.profileImage
return "file://" + PortalService.profileImage;
}
return PortalService.profileImage
return PortalService.profileImage;
}
fallbackIcon: "person"
}
@@ -405,57 +399,58 @@ Item {
anchors.fill: parent
anchors.leftMargin: lockIcon.width + Theme.spacingM * 2
anchors.rightMargin: {
let margin = Theme.spacingM
let margin = Theme.spacingM;
if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width
margin += revealButton.width;
}
if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width
margin += virtualKeyboardButton.width;
}
if (enterButton.visible) {
margin += enterButton.width + 2
margin += enterButton.width + 2;
}
return margin
return margin;
}
opacity: 0
focus: true
echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
onTextChanged: {
if (syncingFromState) return
if (syncingFromState)
return;
if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text
GreeterState.passwordBuffer = text;
} else {
GreeterState.usernameInput = text
GreeterState.usernameInput = text;
}
}
onAccepted: {
if (GreeterState.showPasswordInput) {
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username)
Greetd.createSession(GreeterState.username);
}
} else {
if (text.trim()) {
GreeterState.username = text.trim()
GreeterState.showPasswordInput = true
PortalService.getGreeterUserProfileImage(GreeterState.username)
GreeterState.passwordBuffer = ""
syncingFromState = true
text = ""
syncingFromState = false
GreeterState.username = text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
syncingFromState = true;
text = "";
syncingFromState = false;
}
}
}
Component.onCompleted: {
syncingFromState = true
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput
syncingFromState = false
syncingFromState = true;
text = GreeterState.showPasswordInput ? GreeterState.passwordBuffer : GreeterState.usernameInput;
syncingFromState = false;
if (isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
forceActiveFocus();
}
onVisibleChanged: {
if (visible && isPrimaryScreen && !powerMenu.isVisible)
forceActiveFocus()
forceActiveFocus();
}
}
@@ -475,15 +470,15 @@ Item {
anchors.verticalCenter: parent.verticalCenter
text: {
if (GreeterState.unlocking) {
return "Logging in..."
return "Logging in...";
}
if (Greetd.state !== GreetdState.Inactive) {
return "Authenticating..."
return "Authenticating...";
}
if (GreeterState.showPasswordInput) {
return "Password..."
return "Password...";
}
return "Username..."
return "Username...";
}
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
font.pixelSize: Theme.fontSizeMedium
@@ -513,11 +508,11 @@ Item {
text: {
if (GreeterState.showPasswordInput) {
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
font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium
@@ -558,9 +553,9 @@ Item {
enabled: visible
onClicked: {
if (keyboard_controller.isKeyboardActive) {
keyboard_controller.hide()
keyboard_controller.hide();
} else {
keyboard_controller.show()
keyboard_controller.show();
}
}
}
@@ -578,15 +573,15 @@ Item {
onClicked: {
if (GreeterState.showPasswordInput) {
if (GreeterState.username) {
Greetd.createSession(GreeterState.username)
Greetd.createSession(GreeterState.username);
}
} else {
if (inputField.text.trim()) {
GreeterState.username = inputField.text.trim()
GreeterState.showPasswordInput = true
PortalService.getGreeterUserProfileImage(GreeterState.username)
GreeterState.passwordBuffer = ""
inputField.text = ""
GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
inputField.text = "";
}
}
}
@@ -615,10 +610,10 @@ Item {
Layout.bottomMargin: -Theme.spacingS
text: {
if (GreeterState.pamState === "error")
return "Authentication error - try again"
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password"
return ""
return "Incorrect password";
return "";
}
color: Theme.error
font.pixelSize: Theme.fontSizeSmall
@@ -675,9 +670,9 @@ Item {
cornerRadius: parent.radius
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: {
GreeterState.reset()
inputField.text = ""
PortalService.profileImage = ""
GreeterState.reset();
inputField.text = "";
PortalService.profileImage = "";
}
}
}
@@ -696,11 +691,11 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: {
if (CompositorService.isNiri) {
return NiriService.keyboardLayoutNames.length > 1
return NiriService.keyboardLayoutNames.length > 1;
} else if (CompositorService.isHyprland) {
return hyprlandLayoutCount > 1
return hyprlandLayoutCount > 1;
}
return false
return false;
}
Row {
@@ -726,17 +721,18 @@ Item {
StyledText {
text: {
if (CompositorService.isNiri) {
const layout = NiriService.getCurrentKeyboardLayoutName()
if (!layout) return ""
const parts = layout.split(" ")
const layout = NiriService.getCurrentKeyboardLayoutName();
if (!layout)
return "";
const parts = layout.split(" ");
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) {
return hyprlandCurrentLayout
return hyprlandCurrentLayout;
}
return ""
return "";
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Light
@@ -753,15 +749,10 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout()
NiriService.cycleKeyboardLayout();
} else if (CompositorService.isHyprland) {
Quickshell.execDetached([
"hyprctl",
"switchxkblayout",
hyprlandKeyboard,
"next"
])
updateHyprlandLayout()
Quickshell.execDetached(["hyprctl", "switchxkblayout", hyprlandKeyboard, "next"]);
updateHyprlandLayout();
}
}
}
@@ -773,9 +764,8 @@ Item {
color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter
visible: {
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) ||
(CompositorService.isHyprland && hyprlandLayoutCount > 1)
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available
const keyboardVisible = (CompositorService.isNiri && NiriService.keyboardLayoutNames.length > 1) || (CompositorService.isHyprland && hyprlandLayoutCount > 1);
return keyboardVisible && GreetdSettings.weatherEnabled && WeatherService.weather.available;
}
}
@@ -832,15 +822,15 @@ Item {
DankIcon {
name: {
if (!AudioService.sink?.audio) {
return "volume_up"
return "volume_up";
}
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off"
return "volume_off";
}
if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down"
return "volume_down";
}
return "volume_up"
return "volume_up";
}
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"
@@ -866,95 +856,95 @@ Item {
name: {
if (BatteryService.isCharging) {
if (BatteryService.batteryLevel >= 90) {
return "battery_charging_full"
return "battery_charging_full";
}
if (BatteryService.batteryLevel >= 80) {
return "battery_charging_90"
return "battery_charging_90";
}
if (BatteryService.batteryLevel >= 60) {
return "battery_charging_80"
return "battery_charging_80";
}
if (BatteryService.batteryLevel >= 50) {
return "battery_charging_60"
return "battery_charging_60";
}
if (BatteryService.batteryLevel >= 30) {
return "battery_charging_50"
return "battery_charging_50";
}
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.batteryLevel >= 90) {
return "battery_charging_full"
return "battery_charging_full";
}
if (BatteryService.batteryLevel >= 80) {
return "battery_charging_90"
return "battery_charging_90";
}
if (BatteryService.batteryLevel >= 60) {
return "battery_charging_80"
return "battery_charging_80";
}
if (BatteryService.batteryLevel >= 50) {
return "battery_charging_60"
return "battery_charging_60";
}
if (BatteryService.batteryLevel >= 30) {
return "battery_charging_50"
return "battery_charging_50";
}
if (BatteryService.batteryLevel >= 20) {
return "battery_charging_30"
return "battery_charging_30";
}
return "battery_charging_20"
return "battery_charging_20";
}
if (BatteryService.batteryLevel >= 95) {
return "battery_full"
return "battery_full";
}
if (BatteryService.batteryLevel >= 85) {
return "battery_6_bar"
return "battery_6_bar";
}
if (BatteryService.batteryLevel >= 70) {
return "battery_5_bar"
return "battery_5_bar";
}
if (BatteryService.batteryLevel >= 55) {
return "battery_4_bar"
return "battery_4_bar";
}
if (BatteryService.batteryLevel >= 40) {
return "battery_3_bar"
return "battery_3_bar";
}
if (BatteryService.batteryLevel >= 25) {
return "battery_2_bar"
return "battery_2_bar";
}
return "battery_1_bar"
return "battery_1_bar";
}
size: Theme.iconSize
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
return Theme.error;
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary
return Theme.primary;
}
return "white"
return "white";
}
anchors.verticalCenter: parent.verticalCenter
}
@@ -1007,14 +997,14 @@ Item {
}
property real longestSessionWidth: {
let maxWidth = 0
let maxWidth = 0;
for (var i = 0; i < sessionMetricsRepeater.count; i++) {
const item = sessionMetricsRepeater.itemAt(i)
const item = sessionMetricsRepeater.itemAt(i);
if (item && item.width > maxWidth) {
maxWidth = item.width
maxWidth = item.width;
}
}
return maxWidth
return maxWidth;
}
Repeater {
@@ -1038,52 +1028,42 @@ Item {
openUpwards: true
alignPopupRight: true
onValueChanged: value => {
const idx = GreeterState.sessionList.indexOf(value)
if (idx >= 0) {
GreeterState.currentSessionIndex = idx
GreeterState.selectedSession = GreeterState.sessionExecs[idx]
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[idx])
}
}
const idx = GreeterState.sessionList.indexOf(value);
if (idx >= 0) {
GreeterState.currentSessionIndex = idx;
GreeterState.selectedSession = GreeterState.sessionExecs[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 int pendingParsers: 0
function finalizeSessionSelection() {
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) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i
foundSaved = true
break
GreeterState.currentSessionIndex = i;
foundSaved = true;
break;
}
}
}
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 {
@@ -1091,48 +1071,43 @@ Item {
property string homeDir: Quickshell.env("HOME") || ""
property string xdgDirs: xdgDataDirs || ""
command: {
var paths = [
"/usr/share/wayland-sessions",
"/usr/share/xsessions",
"/usr/local/share/wayland-sessions",
"/usr/local/share/xsessions"
]
var paths = ["/usr/share/wayland-sessions", "/usr/share/xsessions", "/usr/local/share/wayland-sessions", "/usr/local/share/xsessions"];
if (homeDir) {
paths.push(homeDir + "/.local/share/wayland-sessions")
paths.push(homeDir + "/.local/share/xsessions")
paths.push(homeDir + "/.local/share/wayland-sessions");
paths.push(homeDir + "/.local/share/xsessions");
}
// Add XDG_DATA_DIRS paths
if (xdgDirs) {
xdgDirs.split(":").forEach(function(dir) {
xdgDirs.split(":").forEach(function (dir) {
if (dir) {
paths.push(dir + "/wayland-sessions")
paths.push(dir + "/xsessions")
paths.push(dir + "/wayland-sessions");
paths.push(dir + "/xsessions");
}
})
});
}
// 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
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"
return ["sh", "-c", findCmd]
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";
return ["sh", "-c", findCmd];
}
running: false
stdout: SplitParser {
onRead: data => {
if (data.trim()) {
root.pendingParsers++
parseDesktopFile(data.trim())
}
}
if (data.trim()) {
root.pendingParsers++;
parseDesktopFile(data.trim());
}
}
}
}
function parseDesktopFile(path) {
const parser = desktopParser.createObject(null, {
"desktopPath": path
})
"desktopPath": path
});
}
Component {
@@ -1144,42 +1119,41 @@ Item {
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n")
let name = ""
let exec = ""
const lines = text.split("\n");
let name = "";
let exec = "";
for (const line of lines) {
if (line.startsWith("Name=")) {
name = line.substring(5).trim()
name = line.substring(5).trim();
} else if (line.startsWith("Exec=")) {
exec = line.substring(5).trim()
exec = line.substring(5).trim();
}
}
if (name && exec) {
if (!GreeterState.sessionList.includes(name)) {
let newList = GreeterState.sessionList.slice()
let newExecs = GreeterState.sessionExecs.slice()
let newPaths = GreeterState.sessionPaths.slice()
newList.push(name)
newExecs.push(exec)
newPaths.push(desktopPath)
GreeterState.sessionList = newList
GreeterState.sessionExecs = newExecs
GreeterState.sessionPaths = newPaths
root.sessionCount = GreeterState.sessionList.length
let newList = GreeterState.sessionList.slice();
let newExecs = GreeterState.sessionExecs.slice();
let newPaths = GreeterState.sessionPaths.slice();
newList.push(name);
newExecs.push(exec);
newPaths.push(desktopPath);
GreeterState.sessionList = newList;
GreeterState.sessionExecs = newExecs;
GreeterState.sessionPaths = newPaths;
}
}
}
}
onExited: code => {
root.pendingParsers--
if (root.pendingParsers === 0) {
Qt.callLater(root.finalizeSessionSelection)
}
destroy()
}
root.pendingParsers--;
if (root.pendingParsers === 0) {
Qt.callLater(root.finalizeSessionSelection);
}
destroy();
}
}
}
@@ -1189,34 +1163,34 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) {
Greetd.respond(GreeterState.passwordBuffer)
GreeterState.passwordBuffer = ""
inputField.text = ""
Greetd.respond(GreeterState.passwordBuffer);
GreeterState.passwordBuffer = "";
inputField.text = "";
} else if (!error) {
Greetd.respond("")
Greetd.respond("");
}
}
function onReadyToLaunch() {
GreeterState.unlocking = true
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]
GreeterState.unlocking = true;
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
if (sessionCmd) {
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex])
GreetdMemory.setLastSuccessfulUser(GreeterState.username)
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"])
GreetdMemory.setLastSessionId(GreeterState.sessionPaths[GreeterState.currentSessionIndex]);
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
}
}
function onAuthFailure(message) {
GreeterState.pamState = "fail"
GreeterState.passwordBuffer = ""
inputField.text = ""
placeholderDelay.restart()
GreeterState.pamState = "fail";
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
}
function onError(error) {
GreeterState.pamState = "error"
placeholderDelay.restart()
GreeterState.pamState = "error";
placeholderDelay.restart();
}
}
@@ -1231,7 +1205,7 @@ Item {
showLogout: false
onClosed: {
if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
Qt.callLater(() => inputField.forceActiveFocus())
Qt.callLater(() => inputField.forceActiveFocus());
}
}
}

View File

@@ -212,10 +212,12 @@ Item {
color: "transparent"
Item {
anchors.centerIn: parent
anchors.verticalCenterOffset: -100
id: clockContainer
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.verticalCenter
anchors.bottomMargin: 60
width: parent.width
height: 140
height: clockText.implicitHeight
visible: SettingsData.lockScreenShowTime
Row {
@@ -330,8 +332,10 @@ Item {
}
StyledText {
anchors.centerIn: parent
anchors.verticalCenterOffset: -25
id: dateText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: clockContainer.bottom
anchors.topMargin: 4
visible: SettingsData.lockScreenShowDate
text: {
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
@@ -346,8 +350,9 @@ Item {
ColumnLayout {
id: passwordLayout
anchors.centerIn: parent
anchors.verticalCenterOffset: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: dateText.visible ? dateText.bottom : clockContainer.bottom
anchors.topMargin: Theme.spacingL
spacing: Theme.spacingM
width: 380
@@ -384,7 +389,6 @@ Item {
border.width: passwordField.activeFocus ? 2 : 1
visible: SettingsData.lockScreenShowPasswordField || root.passwordBuffer.length > 0
Item {
id: lockIconContainer
anchors.left: parent.left

View File

@@ -9,6 +9,7 @@ DankListView {
property var keyboardController: null
property bool keyboardActive: false
property bool autoScrollDisabled: false
property bool isAnimatingExpansion: false
property alias count: listView.count
property alias listContentHeight: listView.contentHeight
@@ -29,8 +30,19 @@ DankListView {
Timer {
id: positionPreservationTimer
interval: 200
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion
repeat: true
onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled && !isAnimatingExpansion) {
keyboardController.ensureVisible();
}
}
}
Timer {
id: expansionEnsureVisibleTimer
interval: Theme.mediumDuration + 50
repeat: false
onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) {
keyboardController.ensureVisible();
@@ -68,14 +80,7 @@ DankListView {
width: ListView.view.width
height: isDismissing ? 0 : notificationCard.height
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
clip: isDismissing
NotificationCard {
id: notificationCard
@@ -84,6 +89,23 @@ DankListView {
notificationGroup: modelData
keyboardNavigationActive: listView.keyboardActive
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: {
if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive)
@@ -173,23 +195,15 @@ DankListView {
}
function onExpandedGroupsChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(() => {
if (!autoScrollDisabled) {
keyboardController.ensureVisible();
}
});
}
if (!keyboardController || !keyboardController.keyboardNavigationActive)
return;
expansionEnsureVisibleTimer.restart();
}
function onExpandedMessagesChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) {
Qt.callLater(() => {
if (!autoScrollDisabled) {
keyboardController.ensureVisible();
}
});
}
if (!keyboardController || !keyboardController.keyboardNavigationActive)
return;
expansionEnsureVisibleTimer.restart();
}
}
}

View File

@@ -1,8 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import qs.Common
import qs.Services
@@ -13,9 +10,9 @@ Rectangle {
property var notificationGroup
property bool expanded: (NotificationService.expandedGroups[notificationGroup && notificationGroup.key] || false)
property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false)
property bool descriptionExpanded: (NotificationService.expandedMessages[(notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""] || false)
property bool userInitiatedExpansion: false
property bool isAnimating: false
property bool isGroupSelected: false
property int selectedNotificationIndex: -1
@@ -24,13 +21,13 @@ Rectangle {
width: parent ? parent.width : 400
height: {
if (expanded) {
return expandedContent.height + 28
return expandedContent.height + 28;
}
const baseHeight = 116
const baseHeight = 116;
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
@@ -43,36 +40,36 @@ Rectangle {
color: {
if (isGroupSelected && keyboardNavigationActive) {
return Theme.primaryPressed
return Theme.primaryPressed;
}
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: {
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) {
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) {
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: {
if (isGroupSelected && keyboardNavigationActive) {
return 1.5
return 1.5;
}
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return 1
return 1;
}
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return 2
return 2;
}
return 1
return 1;
}
clip: true
@@ -121,21 +118,21 @@ Rectangle {
imageSource: {
if (hasNotificationImage)
return notificationGroup.latestNotification.cleanImage
return notificationGroup.latestNotification.cleanImage;
if (notificationGroup?.latestNotification?.appIcon) {
const appIcon = notificationGroup.latestNotification.appIcon
const appIcon = notificationGroup.latestNotification.appIcon;
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://"))
return appIcon
return Quickshell.iconPath(appIcon, true)
return appIcon;
return Quickshell.iconPath(appIcon, true);
}
return ""
return "";
}
hasImage: hasNotificationImage
fallbackIcon: ""
fallbackText: {
const appName = notificationGroup?.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = notificationGroup?.appName || "?";
return appName.charAt(0).toUpperCase();
}
Rectangle {
@@ -195,9 +192,9 @@ Rectangle {
StyledText {
width: parent.width
text: {
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || ""
const appName = (notificationGroup && notificationGroup.appName) || ""
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName
const timeStr = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "";
const appName = (notificationGroup && notificationGroup.appName) || "";
return timeStr.length > 0 ? `${appName} ${timeStr}` : appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -238,24 +235,23 @@ Rectangle {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (parent.hasMoreText || descriptionExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification
&& notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""
NotificationService.toggleMessageExpansion(messageId)
}
}
if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) {
const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : "";
NotificationService.toggleMessageExpansion(messageId);
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
}
}
}
@@ -335,15 +331,15 @@ Rectangle {
width: parent.width
height: {
const baseHeight = 120
const baseHeight = 120;
if (messageExpanded) {
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2
const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2;
if (bodyText.implicitHeight > twoLineHeight + 2) {
const extraHeight = bodyText.implicitHeight - twoLineHeight
return baseHeight + extraHeight
const extraHeight = bodyText.implicitHeight - twoLineHeight;
return baseHeight + extraHeight;
}
}
return baseHeight
return baseHeight;
}
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -379,23 +375,23 @@ Rectangle {
imageSource: {
if (hasNotificationImage)
return modelData.cleanImage
return modelData.cleanImage;
if (modelData?.appIcon) {
const appIcon = modelData.appIcon
const appIcon = modelData.appIcon;
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: ""
fallbackText: {
const appName = modelData?.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData?.appName || "?";
return appName.charAt(0).toUpperCase();
}
}
@@ -456,22 +452,22 @@ Rectangle {
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : (bodyText.hasMoreText || messageExpanded) ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "")
}
}
if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) {
NotificationService.toggleMessageExpansion(modelData?.notification?.id || "");
}
}
propagateComposedEvents: true
onPressed: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
onReleased: mouse => {
if (parent.hoveredLink) {
mouse.accepted = false
}
}
if (parent.hoveredLink) {
mouse.accepted = false;
}
}
}
}
}
@@ -502,11 +498,11 @@ Rectangle {
StyledText {
id: actionText
text: {
const baseText = modelData.text || "View"
const baseText = modelData.text || "View";
if (keyboardNavigationActive && (isGroupSelected || selectedNotificationIndex >= 0)) {
return `${baseText} (${index + 1})`
return `${baseText} (${index + 1})`;
}
return baseText
return baseText;
}
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -523,7 +519,7 @@ Rectangle {
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
modelData.invoke();
}
}
}
@@ -587,11 +583,11 @@ Rectangle {
StyledText {
id: actionText
text: {
const baseText = modelData.text || "View"
const baseText = modelData.text || "View";
if (keyboardNavigationActive && isGroupSelected) {
return `${baseText} (${index + 1})`
return `${baseText} (${index + 1})`;
}
return baseText
return baseText;
}
color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -608,7 +604,7 @@ Rectangle {
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
modelData.invoke();
}
}
}
@@ -654,8 +650,8 @@ Rectangle {
anchors.fill: parent
visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
root.userInitiatedExpansion = true;
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
}
z: -1
}
@@ -677,8 +673,8 @@ Rectangle {
iconSize: 18
buttonSize: 28
onClicked: {
root.userInitiatedExpansion = true
NotificationService.toggleGroupExpansion(notificationGroup?.key || "")
root.userInitiatedExpansion = true;
NotificationService.toggleGroupExpansion(notificationGroup?.key || "");
}
}
@@ -697,7 +693,14 @@ Rectangle {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
onFinished: root.userInitiatedExpansion = false
onRunningChanged: {
if (running) {
root.isAnimating = true;
} else {
root.isAnimating = false;
root.userInitiatedExpansion = false;
}
}
}
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import qs.Common
import qs.Services
QtObject {
@@ -23,530 +22,518 @@ QtObject {
property bool isRebuilding: false
function rebuildFlatNavigation() {
isRebuilding = true
isRebuilding = true;
const nav = []
const groups = NotificationService.groupedNotifications
const nav = [];
const groups = NotificationService.groupedNotifications;
for (var i = 0; i < groups.length; i++) {
const group = groups[i]
const isExpanded = NotificationService.expandedGroups[group.key] || false
const group = groups[i];
const isExpanded = NotificationService.expandedGroups[group.key] || false;
nav.push({
"type": "group",
"groupIndex": i,
"notificationIndex": -1,
"groupKey": group.key,
"notificationId": ""
})
"type": "group",
"groupIndex": i,
"notificationIndex": -1,
"groupKey": group.key,
"notificationId": ""
});
if (isExpanded) {
const notifications = group.notifications || []
const maxNotifications = Math.min(notifications.length, 10)
const notifications = group.notifications || [];
const maxNotifications = Math.min(notifications.length, 10);
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({
"type": "notification",
"groupIndex": i,
"notificationIndex": j,
"groupKey": group.key,
"notificationId": notifId
})
"type": "notification",
"groupIndex": i,
"notificationIndex": j,
"groupKey": group.key,
"notificationId": notifId
});
}
}
}
flatNavigation = nav
updateSelectedIndexFromId()
isRebuilding = false
flatNavigation = nav;
updateSelectedIndexFromId();
isRebuilding = false;
}
function updateSelectedIndexFromId() {
if (!keyboardNavigationActive) {
return
return;
}
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) {
selectedFlatIndex = i
selectionVersion++ // Trigger UI update
return
selectedFlatIndex = i;
selectionVersion++; // Trigger UI update
return;
} else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) {
selectedFlatIndex = i
selectionVersion++ // Trigger UI update
return
selectedFlatIndex = i;
selectionVersion++; // Trigger UI update
return;
}
}
// If not found, try to find the same group but select the group header instead
if (selectedItemType === "notification") {
for (var j = 0; j < flatNavigation.length; j++) {
const groupItem = flatNavigation[j]
const groupItem = flatNavigation[j];
if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) {
selectedFlatIndex = j
selectedItemType = "group"
selectedNotificationId = ""
selectionVersion++ // Trigger UI update
return
selectedFlatIndex = j;
selectedItemType = "group";
selectedNotificationId = "";
selectionVersion++; // Trigger UI update
return;
}
}
}
// If still not found, clamp to valid range and update
if (flatNavigation.length > 0) {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1)
selectedFlatIndex = Math.max(selectedFlatIndex, 0)
updateSelectedIdFromIndex()
selectionVersion++ // Trigger UI update
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1);
selectedFlatIndex = Math.max(selectedFlatIndex, 0);
updateSelectedIdFromIndex();
selectionVersion++; // Trigger UI update
}
}
function updateSelectedIdFromIndex() {
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) {
const item = flatNavigation[selectedFlatIndex]
selectedItemType = item.type
selectedGroupKey = item.groupKey
selectedNotificationId = item.notificationId
const item = flatNavigation[selectedFlatIndex];
selectedItemType = item.type;
selectedGroupKey = item.groupKey;
selectedNotificationId = item.notificationId;
}
}
function reset() {
selectedFlatIndex = 0
keyboardNavigationActive = false
showKeyboardHints = false
selectedFlatIndex = 0;
keyboardNavigationActive = false;
showKeyboardHints = false;
// Reset keyboardActive when modal is reset
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
rebuildFlatNavigation()
rebuildFlatNavigation();
}
function selectNext() {
keyboardNavigationActive = true
keyboardNavigationActive = true;
if (flatNavigation.length === 0)
return
return;
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
listView.enableAutoScroll();
}
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1)
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1);
updateSelectedIdFromIndex();
selectionVersion++;
ensureVisible();
}
function selectNextWrapping() {
keyboardNavigationActive = true
keyboardNavigationActive = true;
if (flatNavigation.length === 0)
return
return;
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
listView.enableAutoScroll();
}
selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
selectedFlatIndex = (selectedFlatIndex + 1) % flatNavigation.length;
updateSelectedIdFromIndex();
selectionVersion++;
ensureVisible();
}
function selectPrevious() {
keyboardNavigationActive = true
keyboardNavigationActive = true;
if (flatNavigation.length === 0)
return
return;
// Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll()
listView.enableAutoScroll();
}
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0)
updateSelectedIdFromIndex()
selectionVersion++
ensureVisible()
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0);
updateSelectedIdFromIndex();
selectionVersion++;
ensureVisible();
}
function toggleGroupExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
return;
// 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)
return
return;
const wasExpanded = NotificationService.expandedGroups[group.key] || false;
const groupIndex = currentItem.groupIndex;
const wasExpanded = NotificationService.expandedGroups[group.key] || false
const groupIndex = currentItem.groupIndex
isTogglingGroup = true
NotificationService.toggleGroupExpansion(group.key)
rebuildFlatNavigation()
isTogglingGroup = true;
NotificationService.toggleGroupExpansion(group.key);
rebuildFlatNavigation();
// Smart selection after toggle
if (!wasExpanded) {
// Just expanded - move to first notification in the group
for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i
break
selectedFlatIndex = i;
break;
}
}
} else {
// Just collapsed - stay on the group header
for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i
break
selectedFlatIndex = i;
break;
}
}
}
isTogglingGroup = false
ensureVisible()
isTogglingGroup = false;
}
function handleEnterKey() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
return;
if (currentItem.type === "group") {
const notificationCount = group.notifications ? group.notifications.length : 0
const notificationCount = group.notifications ? group.notifications.length : 0;
if (notificationCount >= 2) {
toggleGroupExpanded()
toggleGroupExpanded();
} else {
executeAction(0)
executeAction(0);
}
} else if (currentItem.type === "notification") {
executeAction(0)
executeAction(0);
}
}
function toggleTextExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
let messageId = ""
return;
let messageId = "";
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) {
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc"
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc";
}
if (messageId) {
NotificationService.toggleMessageExpansion(messageId)
NotificationService.toggleMessageExpansion(messageId);
}
}
function executeAction(actionIndex) {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
let actions = []
return;
let actions = [];
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) {
actions = group.notifications[currentItem.notificationIndex]?.actions || []
actions = group.notifications[currentItem.notificationIndex]?.actions || [];
}
if (actionIndex >= 0 && actionIndex < actions.length) {
const action = actions[actionIndex]
const action = actions[actionIndex];
if (action.invoke) {
action.invoke()
action.invoke();
if (onClose)
onClose()
onClose();
}
}
}
function clearSelected() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
const groups = NotificationService.groupedNotifications;
const group = groups[currentItem.groupIndex];
if (!group)
return
return;
if (currentItem.type === "group") {
NotificationService.dismissGroup(group.key)
NotificationService.dismissGroup(group.key);
} else if (currentItem.type === "notification") {
const notification = group.notifications[currentItem.notificationIndex]
NotificationService.dismissNotification(notification)
const notification = group.notifications[currentItem.notificationIndex];
NotificationService.dismissNotification(notification);
}
rebuildFlatNavigation()
rebuildFlatNavigation();
if (flatNavigation.length === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
} else {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1)
updateSelectedIdFromIndex()
ensureVisible()
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1);
updateSelectedIdFromIndex();
ensureVisible();
}
}
function findRepeater(parent) {
if (!parent || !parent.children) {
return null
return null;
}
for (var i = 0; i < parent.children.length; i++) {
const child = parent.children[i]
const child = parent.children[i];
if (child.objectName === "notificationRepeater") {
return child
return child;
}
const found = findRepeater(child)
const found = findRepeater(child);
if (found) {
return found
return found;
}
}
return null
return null;
}
function ensureVisible() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView)
return
const currentItem = flatNavigation[selectedFlatIndex]
return;
const currentItem = flatNavigation[selectedFlatIndex];
if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) {
if (currentItem.type === "notification") {
const groupDelegate = listView.itemAtIndex(currentItem.groupIndex)
const groupDelegate = listView.itemAtIndex(currentItem.groupIndex);
if (groupDelegate && groupDelegate.children && groupDelegate.children.length > 0) {
const notificationCard = groupDelegate.children[0]
const repeater = findRepeater(notificationCard)
const notificationCard = groupDelegate.children[0];
const repeater = findRepeater(notificationCard);
if (repeater && currentItem.notificationIndex < repeater.count) {
const notificationItem = repeater.itemAt(currentItem.notificationIndex)
const notificationItem = repeater.itemAt(currentItem.notificationIndex);
if (notificationItem) {
const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0)
const itemY = itemPos.y
const itemHeight = notificationItem.height
const itemPos = notificationItem.mapToItem(listView.contentItem, 0, 0);
const itemY = itemPos.y;
const itemHeight = notificationItem.height;
const viewportTop = listView.contentY
const viewportBottom = listView.contentY + listView.height
const viewportTop = listView.contentY;
const viewportBottom = listView.contentY + listView.height;
if (itemY < viewportTop) {
listView.contentY = itemY - 20
listView.contentY = itemY - 20;
} else if (itemY + itemHeight > viewportBottom) {
listView.contentY = itemY + itemHeight - listView.height + 20
listView.contentY = itemY + itemHeight - listView.height + 20;
}
}
}
}
} else {
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain)
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Contain);
}
listView.forceLayout()
listView.forceLayout();
}
}
function handleKey(event) {
if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) {
NotificationService.clearAllNotifications()
rebuildFlatNavigation()
NotificationService.clearAllNotifications();
rebuildFlatNavigation();
if (flatNavigation.length === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
} else {
selectedFlatIndex = 0
updateSelectedIdFromIndex()
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
}
selectionVersion++
event.accepted = true
return
selectionVersion++;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) {
if (keyboardNavigationActive) {
keyboardNavigationActive = false
event.accepted = true
keyboardNavigationActive = false;
event.accepted = true;
} else {
if (onClose)
onClose()
event.accepted = true
onClose();
event.accepted = true;
}
} else if (event.key === Qt.Key_Down || event.key === 16777237) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation(); // Ensure we have fresh navigation data
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
// Set keyboardActive on listView to show highlight
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
event.accepted = true
selectionVersion++;
ensureVisible();
event.accepted = true;
} else {
selectNext()
event.accepted = true
selectNext();
event.accepted = true;
}
} else if (event.key === Qt.Key_Up || event.key === 16777235) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation(); // Ensure we have fresh navigation data
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
// Set keyboardActive on listView to show highlight
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
event.accepted = true
selectionVersion++;
ensureVisible();
event.accepted = true;
} else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
// Reset keyboardActive when navigation is disabled
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
selectionVersion++
event.accepted = true
return
selectionVersion++;
event.accepted = true;
return;
} else {
selectPrevious()
event.accepted = true
selectPrevious();
event.accepted = true;
}
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else {
selectNext()
selectNext();
}
event.accepted = true
event.accepted = true;
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
selectionVersion++
selectionVersion++;
} else {
selectPrevious()
selectPrevious();
}
event.accepted = true
event.accepted = true;
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else {
selectNext()
selectNext();
}
event.accepted = true
event.accepted = true;
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
if (!keyboardNavigationActive) {
keyboardNavigationActive = true
rebuildFlatNavigation()
selectedFlatIndex = 0
updateSelectedIdFromIndex()
keyboardNavigationActive = true;
rebuildFlatNavigation();
selectedFlatIndex = 0;
updateSelectedIdFromIndex();
if (listView) {
listView.keyboardActive = true
listView.keyboardActive = true;
}
selectionVersion++
ensureVisible()
selectionVersion++;
ensureVisible();
} else if (selectedFlatIndex === 0) {
keyboardNavigationActive = false
keyboardNavigationActive = false;
if (listView) {
listView.keyboardActive = false
listView.keyboardActive = false;
}
selectionVersion++
selectionVersion++;
} else {
selectPrevious()
selectPrevious();
}
event.accepted = true
event.accepted = true;
} else if (keyboardNavigationActive) {
if (event.key === Qt.Key_Space) {
toggleGroupExpanded()
event.accepted = true
toggleGroupExpanded();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
handleEnterKey()
event.accepted = true
handleEnterKey();
event.accepted = true;
} else if (event.key === Qt.Key_E) {
toggleTextExpanded()
event.accepted = true
toggleTextExpanded();
event.accepted = true;
} else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) {
clearSelected()
event.accepted = true
clearSelected();
event.accepted = true;
} else if (event.key === Qt.Key_Tab) {
selectNext()
event.accepted = true
selectNext();
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
selectPrevious()
event.accepted = true
selectPrevious();
event.accepted = true;
} else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
const actionIndex = event.key - Qt.Key_1
executeAction(actionIndex)
event.accepted = true
const actionIndex = event.key - Qt.Key_1;
executeAction(actionIndex);
event.accepted = true;
}
}
if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints
event.accepted = true
showKeyboardHints = !showKeyboardHints;
event.accepted = true;
}
}
@@ -557,13 +544,13 @@ QtObject {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
}
};
}
const result = flatNavigation[selectedFlatIndex] || {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
}
return result
};
return result;
}
}

View File

@@ -13,31 +13,88 @@ Item {
property bool saving: false
readonly property var maxHistoryOptions: [
{ text: "25", value: 25 },
{ text: "50", value: 50 },
{ text: "100", value: 100 },
{ text: "200", value: 200 },
{ text: "500", value: 500 },
{ text: "1000", value: 1000 }
{
text: "25",
value: 25
},
{
text: "50",
value: 50
},
{
text: "100",
value: 100
},
{
text: "200",
value: 200
},
{
text: "500",
value: 500
},
{
text: "1000",
value: 1000
}
]
readonly property var maxEntrySizeOptions: [
{ text: "1 MB", value: 1048576 },
{ 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 }
{
text: "1 MB",
value: 1048576
},
{
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: [
{ text: I18n.tr("Never"), value: 0 },
{ text: I18n.tr("1 day"), 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 }
{
text: I18n.tr("Never"),
value: 0
},
{
text: I18n.tr("1 day"),
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) {
@@ -139,9 +196,7 @@ Item {
StyledText {
font.pixelSize: Theme.fontSizeSmall
text: !DMSService.isConnected
? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.")
: I18n.tr("Failed to load clipboard configuration.")
text: !DMSService.isConnected ? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.") : I18n.tr("Failed to load clipboard configuration.")
wrapMode: Text.WordWrap
width: parent.width - Theme.iconSizeSmall - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
@@ -264,7 +319,7 @@ Item {
settingKey: "disablePersist"
text: I18n.tr("Disable Clipboard Ownership")
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)
}
}

View File

@@ -237,7 +237,10 @@ Item {
visible: defaultBar.visible ?? true,
popupGapsAuto: defaultBar.popupGapsAuto ?? true,
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);
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 {
iconName: "space_bar"
title: I18n.tr("Spacing")

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}
}
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -6,128 +6,21 @@ import qs.Services
import qs.Widgets
Item {
id: displaysTab
id: root
function formatGammaTime(isoString) {
if (!isoString)
return "";
return ""
try {
const date = new Date(isoString);
const date = new Date(isoString)
if (isNaN(date.getTime()))
return "";
return date.toLocaleTimeString(Qt.locale(), "HH:mm");
return ""
return date.toLocaleTimeString(Qt.locale(), "HH:mm")
} 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 {
anchors.fill: parent
clip: true
@@ -181,16 +74,17 @@ Item {
width: parent.width
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
enabled: DisplayService.gammaControlAvailable
onToggled: checked => {
DisplayService.toggleNightMode();
}
DisplayService.toggleNightMode()
}
Connections {
function onNightModeEnabledChanged() {
nightModeToggle.checked = DisplayService.nightModeEnabled;
nightModeToggle.checked = DisplayService.nightModeEnabled
}
target: DisplayService
@@ -210,19 +104,19 @@ Item {
description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply")
currentValue: SessionData.nightModeTemperature + "K"
options: {
var temps = [];
var temps = []
for (var i = 2500; i <= 6000; i += 500) {
temps.push(i + "K");
temps.push(i + "K")
}
return temps;
return temps
}
onValueChanged: value => {
var temp = parseInt(value.replace("K", ""));
SessionData.setNightModeTemperature(temp);
if (SessionData.nightModeHighTemperature < temp) {
SessionData.setNightModeHighTemperature(temp);
}
}
var temp = parseInt(value.replace("K", ""))
SessionData.setNightModeTemperature(temp)
if (SessionData.nightModeHighTemperature < temp) {
SessionData.setNightModeHighTemperature(temp)
}
}
}
DankDropdown {
@@ -232,19 +126,19 @@ Item {
currentValue: SessionData.nightModeHighTemperature + "K"
visible: SessionData.nightModeAutoEnabled
options: {
var temps = [];
var minTemp = SessionData.nightModeTemperature;
var temps = []
var minTemp = SessionData.nightModeTemperature
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 => {
var temp = parseInt(value.replace("K", ""));
if (temp >= SessionData.nightModeTemperature) {
SessionData.setNightModeHighTemperature(temp);
}
}
var temp = parseInt(value.replace("K", ""))
if (temp >= SessionData.nightModeTemperature) {
SessionData.setNightModeHighTemperature(temp)
}
}
}
}
@@ -256,18 +150,18 @@ Item {
checked: SessionData.nightModeAutoEnabled
visible: DisplayService.gammaControlAvailable
onToggled: checked => {
if (checked && !DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode();
} else if (!checked && DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode();
}
SessionData.setNightModeAutoEnabled(checked);
}
if (checked && !DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
} else if (!checked && DisplayService.nightModeEnabled) {
DisplayService.toggleNightMode()
}
SessionData.setNightModeAutoEnabled(checked)
}
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticToggle.checked = SessionData.nightModeAutoEnabled;
automaticToggle.checked = SessionData.nightModeAutoEnabled
}
}
}
@@ -281,7 +175,7 @@ Item {
Connections {
target: SessionData
function onNightModeAutoEnabledChanged() {
automaticSettings.visible = SessionData.nightModeAutoEnabled;
automaticSettings.visible = SessionData.nightModeAutoEnabled
}
}
@@ -294,32 +188,29 @@ Item {
width: 200
height: 45
anchors.horizontalCenter: parent.horizontalCenter
model: [
{
model: [{
"text": "Time",
"icon": "access_time"
},
{
}, {
"text": "Location",
"icon": "place"
}
]
}]
Component.onCompleted: {
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0;
Qt.callLater(updateIndicator);
currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(updateIndicator)
}
onTabClicked: index => {
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time");
currentIndex = index;
}
DisplayService.setNightModeAutomationMode(index === 1 ? "location" : "time")
currentIndex = index
}
Connections {
target: SessionData
function onNightModeAutoModeChanged() {
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0;
Qt.callLater(modeTabBarNight.updateIndicator);
modeTabBarNight.currentIndex = SessionData.nightModeAutoMode === "location" ? 1 : 0
Qt.callLater(modeTabBarNight.updateIndicator)
}
}
}
@@ -376,30 +267,30 @@ Item {
dropdownWidth: 70
currentValue: SessionData.nightModeStartHour.toString()
options: {
var hours = [];
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString());
hours.push(i.toString())
}
return hours;
return hours
}
onValueChanged: value => {
SessionData.setNightModeStartHour(parseInt(value));
}
SessionData.setNightModeStartHour(parseInt(value))
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeStartMinute.toString().padStart(2, '0')
options: {
var minutes = [];
var minutes = []
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 => {
SessionData.setNightModeStartMinute(parseInt(value));
}
SessionData.setNightModeStartMinute(parseInt(value))
}
}
}
@@ -419,30 +310,30 @@ Item {
dropdownWidth: 70
currentValue: SessionData.nightModeEndHour.toString()
options: {
var hours = [];
var hours = []
for (var i = 0; i < 24; i++) {
hours.push(i.toString());
hours.push(i.toString())
}
return hours;
return hours
}
onValueChanged: value => {
SessionData.setNightModeEndHour(parseInt(value));
}
SessionData.setNightModeEndHour(parseInt(value))
}
}
DankDropdown {
dropdownWidth: 70
currentValue: SessionData.nightModeEndMinute.toString().padStart(2, '0')
options: {
var minutes = [];
var minutes = []
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 => {
SessionData.setNightModeEndMinute(parseInt(value));
}
SessionData.setNightModeEndMinute(parseInt(value))
}
}
}
}
@@ -461,13 +352,13 @@ Item {
description: I18n.tr("Automatically detect location based on IP address")
checked: SessionData.nightModeUseIPLocation || false
onToggled: checked => {
SessionData.setNightModeUseIPLocation(checked);
}
SessionData.setNightModeUseIPLocation(checked)
}
Connections {
target: SessionData
function onNightModeUseIPLocationChanged() {
ipLocationToggle.checked = SessionData.nightModeUseIPLocation;
ipLocationToggle.checked = SessionData.nightModeUseIPLocation
}
}
}
@@ -502,9 +393,9 @@ Item {
text: SessionData.latitude.toString()
placeholderText: "0.0"
onEditingFinished: {
const lat = parseFloat(text);
const lat = parseFloat(text)
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()
placeholderText: "0.0"
onEditingFinished: {
const lon = parseFloat(text);
const lon = parseFloat(text)
if (!isNaN(lon) && lon >= -180 && lon <= 180 && lon !== SessionData.longitude) {
SessionData.setLongitude(lon);
SessionData.setLongitude(lon)
}
}
}
@@ -678,7 +569,7 @@ Item {
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunriseTime)
text: root.formatGammaTime(DisplayService.gammaSunriseTime)
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
@@ -714,7 +605,7 @@ Item {
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaSunsetTime)
text: root.formatGammaTime(DisplayService.gammaSunsetTime)
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
@@ -761,7 +652,7 @@ Item {
}
StyledText {
text: displaysTab.formatGammaTime(DisplayService.gammaNextTransition)
text: root.formatGammaTime(DisplayService.gammaNextTransition)
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
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);
}
}
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -229,7 +229,7 @@ Item {
DankTextField {
id: searchField
width: parent.width - addButton.width - Theme.spacingM
height: 44
height: Math.round(Theme.fontSizeMedium * 3)
placeholderText: I18n.tr("Search keybinds...")
leftIconName: "search"
onTextChanged: {
@@ -240,8 +240,8 @@ Item {
DankActionButton {
id: addButton
width: 44
height: 44
width: searchField.height
height: searchField.height
circular: false
iconName: "add"
iconSize: Theme.iconSize
@@ -328,36 +328,21 @@ Item {
}
}
Rectangle {
DankButton {
id: fixButton
width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: 36
radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
text: {
if (KeybindsService.fixing)
return I18n.tr("Fixing...")
if (warningBox.showSetup)
return I18n.tr("Setup")
return I18n.tr("Fix Now")
}
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !KeybindsService.fixing
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: fixButtonText
text: {
if (KeybindsService.fixing)
return I18n.tr("Fixing...");
if (warningBox.showSetup)
return I18n.tr("Setup");
return I18n.tr("Fix Now");
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surface
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: !KeybindsService.fixing
onClicked: KeybindsService.fixDmsBindsInclude()
}
onClicked: KeybindsService.fixDmsBindsInclude()
}
}
}
@@ -382,9 +367,10 @@ Item {
spacing: Theme.spacingS
Rectangle {
readonly property real chipHeight: allChip.implicitHeight + Theme.spacingM
width: allChip.implicitWidth + Theme.spacingL
height: 32
radius: 16
height: chipHeight
radius: chipHeight / 2
color: !keybindsTab.selectedCategory ? Theme.primary : Theme.surfaceContainerHighest
StyledText {
@@ -412,9 +398,10 @@ Item {
required property string modelData
required property int index
readonly property real chipHeight: catText.implicitHeight + Theme.spacingM
width: catText.implicitWidth + Theme.spacingL
height: 32
radius: 16
height: chipHeight
radius: chipHeight / 2
color: keybindsTab.selectedCategory === modelData ? Theme.primary : (modelData === "__overrides__" ? Theme.withAlpha(Theme.primary, 0.15) : Theme.surfaceContainerHighest)
StyledText {

View File

@@ -23,6 +23,15 @@ Item {
iconName: "refresh"
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 {
text: I18n.tr("Use Custom Command")
description: I18n.tr("Use custom command for update your system")

View File

@@ -371,9 +371,14 @@ Item {
widgetObj.pciId = "";
}
if (widgetId === "controlCenterButton") {
widgetObj.showNetworkIcon = true;
widgetObj.showBluetoothIcon = true;
widgetObj.showAudioIcon = true;
widgetObj.showNetworkIcon = SettingsData.controlCenterShowNetworkIcon;
widgetObj.showBluetoothIcon = SettingsData.controlCenterShowBluetoothIcon;
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")
widgetObj.mountPath = "/";
@@ -423,9 +428,14 @@ Item {
else if (widget.id === "gpuTemp")
newWidget.pciId = "";
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
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;
break;
@@ -471,9 +481,14 @@ Item {
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
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;
setWidgetsForSection(sectionId, widgets);
@@ -541,41 +556,48 @@ Item {
if (widget.pciId !== undefined)
newWidget.pciId = widget.pciId;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
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;
setWidgetsForSection(sectionId, widgets);
}
function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) {
switch (settingName) {
case "showNetworkIcon":
SettingsData.set("controlCenterShowNetworkIcon", value);
break;
case "showBluetoothIcon":
SettingsData.set("controlCenterShowBluetoothIcon", value);
break;
case "showAudioIcon":
SettingsData.set("controlCenterShowAudioIcon", value);
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 widgets = getWidgetsForSection(sectionId).slice();
if (widgetIndex < 0 || widgetIndex >= widgets.length)
return;
var widget = widgets[widgetIndex];
if (typeof widget === "string") {
widget = {
"id": widget,
"enabled": true
};
}
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) {
@@ -626,9 +648,14 @@ Item {
if (widget.showSwap !== undefined)
newWidget.showSwap = widget.showSwap;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
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;
setWidgetsForSection(sectionId, widgets);
@@ -678,9 +705,14 @@ Item {
if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
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;
setWidgetsForSection(sectionId, widgets);
@@ -730,9 +762,14 @@ Item {
if (widget.keyboardLayoutNameCompactMode !== undefined)
newWidget.keyboardLayoutNameCompactMode = widget.keyboardLayoutNameCompactMode;
if (widget.id === "controlCenterButton") {
newWidget.showNetworkIcon = widget.showNetworkIcon ?? true;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? true;
newWidget.showAudioIcon = widget.showAudioIcon ?? true;
newWidget.showNetworkIcon = widget.showNetworkIcon ?? SettingsData.controlCenterShowNetworkIcon;
newWidget.showBluetoothIcon = widget.showBluetoothIcon ?? SettingsData.controlCenterShowBluetoothIcon;
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;
widget = newWidget;
@@ -789,6 +826,16 @@ Item {
item.showBluetoothIcon = widget.showBluetoothIcon;
if (widget.showAudioIcon !== undefined)
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)
item.minimumWidth = widget.minimumWidth;
if (widget.showSwap !== undefined)

View File

@@ -806,50 +806,42 @@ Column {
{
icon: "lan",
label: I18n.tr("Network"),
setting: "showNetworkIcon",
checked: SettingsData.controlCenterShowNetworkIcon
setting: "showNetworkIcon"
},
{
icon: "vpn_lock",
label: I18n.tr("VPN"),
setting: "showVpnIcon",
checked: SettingsData.controlCenterShowVpnIcon
setting: "showVpnIcon"
},
{
icon: "bluetooth",
label: I18n.tr("Bluetooth"),
setting: "showBluetoothIcon",
checked: SettingsData.controlCenterShowBluetoothIcon
setting: "showBluetoothIcon"
},
{
icon: "volume_up",
label: I18n.tr("Audio"),
setting: "showAudioIcon",
checked: SettingsData.controlCenterShowAudioIcon
setting: "showAudioIcon"
},
{
icon: "mic",
label: I18n.tr("Microphone"),
setting: "showMicIcon",
checked: SettingsData.controlCenterShowMicIcon
setting: "showMicIcon"
},
{
icon: "brightness_high",
label: I18n.tr("Brightness"),
setting: "showBrightnessIcon",
checked: SettingsData.controlCenterShowBrightnessIcon
setting: "showBrightnessIcon"
},
{
icon: "battery_full",
label: I18n.tr("Battery"),
setting: "showBatteryIcon",
checked: SettingsData.controlCenterShowBatteryIcon
setting: "showBatteryIcon"
},
{
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon",
checked: SettingsData.controlCenterShowPrinterIcon
setting: "showPrinterIcon"
}
]
@@ -857,6 +849,30 @@ Column {
required property var modelData
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
height: 32
radius: Theme.cornerRadius
@@ -891,7 +907,7 @@ Column {
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: modelData.checked
checked: getCheckedState()
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled);
}

View File

@@ -216,8 +216,9 @@ Variants {
}
readonly property int maxTextureSize: 8192
property int textureWidth: Math.min(modelData.width, maxTextureSize)
property int textureHeight: Math.min(modelData.height, maxTextureSize)
property real screenScale: CompositorService.getScreenScale(modelData)
property int textureWidth: Math.min(Math.round(modelData.width * screenScale), maxTextureSize)
property int textureHeight: Math.min(Math.round(modelData.height * screenScale), maxTextureSize)
Image {
id: currentWallpaper

View File

@@ -30,7 +30,7 @@ Singleton {
id: cavaProcess
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: {
if (!running) {

View File

@@ -53,10 +53,13 @@ Singleton {
signal gammaStateUpdate(var data)
signal openUrlRequested(string url)
signal appPickerRequested(var data)
signal screensaverStateUpdate(var data)
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: {
if (socketPath && socketPath.length > 0) {
@@ -371,6 +374,10 @@ Singleton {
} else if (data.url) {
openUrlRequested(data.url);
}
} else if (service === "freedesktop.screensaver") {
screensaverInhibited = data.inhibited || false;
screensaverInhibitors = data.inhibitors || [];
screensaverStateUpdate(data);
}
}

View File

@@ -1,13 +1,19 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
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 var outputs: ({})
property var tagCount: 9
@@ -263,4 +269,85 @@ Singleton {
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
}
}
}

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"
}
}
}

View File

@@ -29,6 +29,8 @@ Singleton {
property bool respectInhibitors: true
property bool _enableGate: true
readonly property bool externalInhibitActive: DMSService.screensaverInhibited
readonly property bool isOnBattery: BatteryService.batteryAvailable && !BatteryService.isPluggedIn
readonly property int monitorTimeout: isOnBattery ? SettingsData.batteryMonitorTimeout : SettingsData.acMonitorTimeout
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: {
if (!idleMonitorAvailable) {
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");
createIdleMonitors();
}
if (externalInhibitActive) {
const apps = DMSService.screensaverInhibitors.map(i => i.appName).join(", ");
SessionService.idleInhibited = true;
SessionService.inhibitReason = "External app: " + (apps || "unknown");
}
}
}

View File

@@ -1,5 +1,5 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtCore
import QtQuick
@@ -23,6 +23,8 @@ Singleton {
property var windows: []
property var displayScales: ({})
property var _realOutputs: ({})
property bool inOverview: false
property int currentKeyboardLayoutIndex: 0
@@ -66,6 +68,19 @@ Singleton {
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 {
id: validateProcess
command: ["niri", "validate"]
@@ -201,12 +216,12 @@ Singleton {
const ws = workspaces[w.workspace_id];
if (!ws) {
return {
window: w,
outputX: 999999,
outputY: 999999,
wsIdx: 999999,
col: 999999,
row: 999999
"window": w,
"outputX": 999999,
"outputY": 999999,
"wsIdx": 999999,
"col": 999999,
"row": 999999
};
}
@@ -219,12 +234,12 @@ Singleton {
const row = (pos && pos.length >= 2) ? pos[1] : 999999;
return {
window: w,
outputX: outputX,
outputY: outputY,
wsIdx: ws.idx,
col: col,
row: row
"window": w,
"outputX": outputX,
"outputY": outputY,
"wsIdx": ws.idx,
"col": col,
"row": row
};
});
@@ -291,6 +306,9 @@ Singleton {
case 'WorkspaceUrgencyChanged':
handleWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged);
break;
case 'WindowUrgencyChanged':
handleWindowUrgencyChanged(event.WindowUrgencyChanged);
break;
case 'ScreenshotCaptured':
handleScreenshotCaptured(event.ScreenshotCaptured);
break;
@@ -558,6 +576,23 @@ Singleton {
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) {
if (!data.path)
return;
@@ -565,7 +600,7 @@ Singleton {
const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
const command = editor === "satty" ? ["satty", "-f", data.path] : ["swappy", "-f", data.path];
Quickshell.execDetached({
command: command
"command": command
});
pendingScreenshotPath = "";
}
@@ -959,29 +994,36 @@ Singleton {
const cornerRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const gaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const configContent = `layout {
gaps ${gaps}
const dmsWarning = `// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
border {
`;
const configContent = dmsWarning + `layout {
gaps ${gaps}
border {
width 2
}
}
focus-ring {
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius ${cornerRadius}
clip-to-geometry true
tiled-state true
draw-border-with-background false
}`;
}
}
window-rule {
geometry-corner-radius ${cornerRadius}
clip-to-geometry true
tiled-state true
draw-border-with-background false
}`;
const alttabContent = `recent-windows {
highlight {
const alttabContent = dmsWarning + `recent-windows {
highlight {
corner-radius ${cornerRadius}
}
}`;
}
}`;
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const niriDmsDir = configDir + "/niri/dms";
@@ -1014,6 +1056,141 @@ window-rule {
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 {
function screenshot(): string {
if (!CompositorService.isNiri) {

View File

@@ -6,6 +6,7 @@ import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Common
import qs.Services
Singleton {
id: root
@@ -20,6 +21,7 @@ Singleton {
}
return false;
}
readonly property bool shouldPauseCycling: anyFullscreen || SessionService.locked
property string cachedCyclingTime: SessionData.wallpaperCyclingTime
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
property string lastTimeCheck: ""
@@ -34,7 +36,7 @@ Singleton {
running: false
repeat: true
onTriggered: {
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.anyFullscreen) {
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.shouldPauseCycling) {
WallpaperCyclingService.cycleNextForMonitor(targetScreen);
}
}
@@ -113,6 +115,16 @@ Singleton {
}
}
Connections {
target: SessionService
function onSessionUnlocked() {
if (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper) {
updateCyclingState();
}
}
}
function updateCyclingState() {
if (SessionData.perMonitorWallpaper) {
stopCycling();
@@ -151,13 +163,18 @@ Singleton {
}
function startCycling() {
if (SessionData.wallpaperCyclingMode === "interval") {
switch (SessionData.wallpaperCyclingMode) {
case "interval":
lastTimeCheck = "";
intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.start();
cyclingActive = true;
} else if (SessionData.wallpaperCyclingMode === "time") {
break;
case "time":
intervalTimer.stop();
cyclingActive = true;
checkTimeBasedCycling();
break;
}
}
@@ -167,23 +184,43 @@ Singleton {
}
function startMonitorCycling(screenName, settings) {
if (settings.mode === "interval") {
var timer = monitorTimers[screenName];
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
var newTimers = Object.assign({}, monitorTimers);
newTimers[screenName] = monitorTimerComponent.createObject(root);
newTimers[screenName].targetScreen = screenName;
monitorTimers = newTimers;
timer = monitorTimers[screenName];
switch (settings.mode) {
case "interval":
{
var newChecks = Object.assign({}, monitorLastTimeChecks);
delete newChecks[screenName];
monitorLastTimeChecks = newChecks;
var timer = monitorTimers[screenName];
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
var newTimers = Object.assign({}, monitorTimers);
newTimers[screenName] = monitorTimerComponent.createObject(root);
newTimers[screenName].targetScreen = screenName;
monitorTimers = newTimers;
timer = monitorTimers[screenName];
}
if (timer) {
timer.interval = settings.interval * 1000;
timer.start();
}
break;
}
if (timer) {
timer.interval = settings.interval * 1000;
timer.start();
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);
newChecks[screenName] = "";
monitorLastTimeChecks = newChecks;
break;
}
} else if (settings.mode === "time") {
var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = "";
monitorLastTimeChecks = newChecks;
}
}
@@ -319,7 +356,7 @@ Singleton {
}
function checkTimeBasedCycling() {
if (anyFullscreen)
if (shouldPauseCycling)
return;
const currentTime = Qt.formatTime(systemClock.date, "hh:mm");
@@ -367,7 +404,7 @@ Singleton {
running: false
repeat: true
onTriggered: {
if (anyFullscreen)
if (shouldPauseCycling)
return;
cycleToNextWallpaper();
}

View File

@@ -1,3 +1,8 @@
// ! DO NOT EDIT !
// ! AUTO-GENERATED BY DMS !
// ! CHANGES WILL BE OVERWRITTEN !
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
layer-rule {
match namespace="dms:blurwallpaper"
place-within-backdrop true

View File

@@ -74,7 +74,7 @@ PanelWindow {
readonly property real dpr: CompositorService.getScreenScale(screen)
readonly property real screenWidth: screen.width
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 alignedHeight: Theme.px(osdHeight, dpr)

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
@@ -13,7 +12,7 @@ StyledRect {
onActiveFocusChanged: {
if (activeFocus) {
textInput.forceActiveFocus()
textInput.forceActiveFocus();
}
}
@@ -53,26 +52,26 @@ StyledRect {
signal focusStateChanged(bool hasFocus)
function getActiveFocus() {
return textInput.activeFocus
return textInput.activeFocus;
}
function setFocus(value) {
textInput.focus = value
textInput.focus = value;
}
function forceActiveFocus() {
textInput.forceActiveFocus()
textInput.forceActiveFocus();
}
function selectAll() {
textInput.selectAll()
textInput.selectAll();
}
function clear() {
textInput.clear()
textInput.clear();
}
function insertText(str) {
textInput.insert(textInput.cursorPosition, str)
textInput.insert(textInput.cursorPosition, str);
}
width: 200
height: 48
height: Math.round(Theme.fontSizeMedium * 3.4)
radius: cornerRadius
color: backgroundColor
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
@@ -112,30 +111,30 @@ StyledRect {
onActiveFocusChanged: root.focusStateChanged(activeFocus)
Keys.forwardTo: root.keyForwardTargets
Keys.onLeftPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true
} else {
// Allow normal TextInput cursor movement
event.accepted = false
}
}
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
// Allow normal TextInput cursor movement
event.accepted = false;
}
}
Keys.onRightPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true
} else {
event.accepted = false
}
}
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
event.accepted = false;
}
}
Keys.onPressed: event => {
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i]) {
root.keyForwardTargets[i].Keys.pressed(event)
}
}
}
}
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false;
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i]) {
root.keyForwardTargets[i].Keys.pressed(event);
}
}
}
}
MouseArea {
anchors.fill: parent
@@ -171,7 +170,7 @@ StyledRect {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
textInput.text = ""
textInput.text = "";
}
}
}

View File

@@ -54,6 +54,12 @@ Item {
readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
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 saveBind(string originalKey, var newData)
signal removeBind(string key)
@@ -223,7 +229,7 @@ Item {
Rectangle {
id: collapsedRect
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
topLeftRadius: Theme.cornerRadius
topRightRadius: Theme.cornerRadius
@@ -240,7 +246,7 @@ Item {
Column {
id: keysColumn
Layout.preferredWidth: 140
Layout.preferredWidth: root._keysColumnWidth
Layout.alignment: Qt.AlignVCenter
spacing: Theme.spacingXS
@@ -253,9 +259,9 @@ Item {
property bool isSelected: root.isExpanded && root.editingKeyIndex === index && !root.addingNewKey
width: 140
height: 28
radius: 6
width: root._keysColumnWidth
height: root._chipHeight
radius: root._chipHeight / 4
color: isSelected ? Theme.primary : Theme.surfaceVariant
Rectangle {
@@ -332,7 +338,7 @@ Item {
DankIcon {
name: "warning"
size: 14
size: Theme.iconSizeSmall
color: Theme.primary
visible: root.hasConfigConflict
}
@@ -352,7 +358,7 @@ Item {
DankIcon {
name: root.isExpanded ? "expand_less" : "expand_more"
size: 20
size: Theme.iconSize - 4
color: Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter
}
@@ -360,7 +366,7 @@ Item {
MouseArea {
anchors.fill: parent
anchors.leftMargin: 140 + Theme.spacingM * 2
anchors.leftMargin: root._keysColumnWidth + Theme.spacingM * 2
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleExpand()
}
@@ -420,7 +426,7 @@ Item {
DankIcon {
name: "warning"
size: 16
size: Theme.iconSizeSmall
color: Theme.primary
}
@@ -461,7 +467,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
Flow {
@@ -478,8 +484,8 @@ Item {
property bool isSelected: root.editingKeyIndex === index && !root.addingNewKey
width: editKeyChipText.implicitWidth + Theme.spacingM
height: 28
radius: 6
height: root._chipHeight
radius: root._chipHeight / 4
color: isSelected ? Theme.primary : Theme.surfaceVariant
Rectangle {
@@ -509,9 +515,9 @@ Item {
}
Rectangle {
width: 28
height: 28
radius: 6
width: root._chipHeight
height: root._chipHeight
radius: root._chipHeight / 4
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: !root.isNew
@@ -523,7 +529,7 @@ Item {
DankIcon {
name: "add"
size: 16
size: Theme.iconSizeSmall
color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -548,13 +554,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
FocusScope {
id: captureScope
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
focus: root.recording
Component.onCompleted: {
@@ -596,12 +602,12 @@ Item {
DankActionButton {
id: recordBtn
width: 28
height: 28
width: root._chipHeight
height: root._chipHeight
anchors.verticalCenter: parent.verticalCenter
circular: false
iconName: root.recording ? "close" : "radio_button_checked"
iconSize: 16
iconSize: Theme.iconSizeSmall
iconColor: root.recording ? Theme.error : Theme.primary
onClicked: root.recording ? root.stopRecording() : root.startRecording()
}
@@ -703,8 +709,8 @@ Item {
}
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius
color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
visible: root.keys.length === 1 && !root.isNew
@@ -717,7 +723,7 @@ Item {
DankIcon {
name: "add"
size: 18
size: Theme.iconSizeSmall + 2
color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -736,11 +742,11 @@ Item {
Layout.fillWidth: true
spacing: Theme.spacingS
visible: root.hasConflict
Layout.leftMargin: 60 + Theme.spacingM
Layout.leftMargin: root._labelWidth + Theme.spacingM
DankIcon {
name: "warning"
size: 16
size: Theme.iconSizeSmall
color: Theme.primary
}
@@ -762,7 +768,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
RowLayout {
@@ -785,7 +791,7 @@ Item {
})
Layout.fillWidth: true
Layout.preferredHeight: 36
Layout.preferredHeight: root._buttonHeight
radius: Theme.cornerRadius
color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
@@ -797,7 +803,7 @@ Item {
DankIcon {
name: typeDelegate.modelData.icon
size: 16
size: Theme.iconSizeSmall
color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
}
@@ -869,7 +875,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankDropdown {
@@ -913,14 +919,14 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
visible: dmsArgsRow.hasAmountArg
}
DankTextField {
id: dmsAmountField
Layout.preferredWidth: 80
Layout.preferredHeight: 40
Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 5.5)
Layout.preferredHeight: root._inputHeight
placeholderText: "5"
visible: dmsArgsRow.hasAmountArg
@@ -961,14 +967,14 @@ Item {
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.leftMargin: dmsArgsRow.hasAmountArg ? Theme.spacingM : 0
Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : 60
Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : root._labelWidth
visible: dmsArgsRow.hasDeviceArg
}
DankTextField {
id: dmsDeviceField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("leave empty for default")
visible: dmsArgsRow.hasDeviceArg
@@ -1006,7 +1012,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
visible: dmsArgsRow.hasTabArg
}
@@ -1064,12 +1070,12 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankDropdown {
id: compositorCatDropdown
Layout.preferredWidth: 120
Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 8.5)
compactMode: true
currentValue: {
const base = root.editAction.split(" ")[0];
@@ -1108,8 +1114,8 @@ Item {
}
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius
color: Theme.surfaceVariant
@@ -1121,7 +1127,7 @@ Item {
DankIcon {
name: "edit"
size: 18
size: Theme.iconSizeSmall + 2
color: Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -1150,7 +1156,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
RowLayout {
@@ -1160,7 +1166,7 @@ Item {
DankTextField {
id: argValueField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
visible: {
const cfg = optionsRow.argConfig;
if (!cfg?.config?.args)
@@ -1308,13 +1314,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: customCompositorField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., focus-workspace 3, resize-column -10")
text: root._actionType === "compositor" ? root.editAction : ""
onTextChanged: {
@@ -1327,8 +1333,8 @@ Item {
}
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.preferredWidth: root._inputHeight
Layout.preferredHeight: root._inputHeight
radius: Theme.cornerRadius
color: Theme.surfaceVariant
@@ -1340,7 +1346,7 @@ Item {
DankIcon {
name: "list"
size: 18
size: Theme.iconSizeSmall + 2
color: Theme.surfaceVariantText
anchors.centerIn: parent
}
@@ -1371,13 +1377,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: spawnTextField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., firefox, kitty --title foo")
readonly property var _parsed: root._actionType === "spawn" ? Actions.parseSpawnCommand(root.editAction) : null
text: _parsed ? (_parsed.command + " " + _parsed.args.join(" ")).trim() : ""
@@ -1403,13 +1409,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: shellTextField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1")
text: root._actionType === "shell" ? Actions.parseShellCommand(root.editAction) : ""
onTextChanged: {
@@ -1431,13 +1437,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: titleField
Layout.fillWidth: true
Layout.preferredHeight: 40
Layout.preferredHeight: root._inputHeight
placeholderText: I18n.tr("Hotkey overlay title (optional)")
text: root.editDesc
onTextChanged: root.updateEdit({
@@ -1455,13 +1461,13 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
Layout.preferredWidth: 60
Layout.preferredWidth: root._labelWidth
}
DankTextField {
id: cooldownField
Layout.preferredWidth: 100
Layout.preferredHeight: 40
Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 7)
Layout.preferredHeight: root._inputHeight
placeholderText: "0"
Connections {
@@ -1508,8 +1514,8 @@ Item {
spacing: Theme.spacingM
DankActionButton {
Layout.preferredWidth: 32
Layout.preferredHeight: 32
Layout.preferredWidth: root._buttonHeight
Layout.preferredHeight: root._buttonHeight
circular: false
iconName: "delete"
iconSize: Theme.iconSize - 4
@@ -1531,7 +1537,7 @@ Item {
DankButton {
text: I18n.tr("Cancel")
buttonHeight: 32
buttonHeight: root._buttonHeight
backgroundColor: Theme.surfaceContainer
textColor: Theme.surfaceText
visible: root.hasChanges || root.isNew
@@ -1547,7 +1553,7 @@ Item {
DankButton {
text: root.isNew ? I18n.tr("Add") : I18n.tr("Save")
buttonHeight: 32
buttonHeight: root._buttonHeight
enabled: root.canSave()
visible: root.hasChanges || root.isNew
onClicked: root.doSave()

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
@@ -13,7 +14,7 @@ Rectangle {
property string expandedUuid: ""
property int listHeight: 180
implicitHeight: contentColumn.implicitHeight + Theme.spacingM * 2
implicitHeight: 32 + 1 + listHeight + Theme.spacingS * 4 + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -153,328 +154,71 @@ Rectangle {
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
Item {
width: parent.width
height: root.listHeight
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: 4
anchors.centerIn: parent
spacing: Theme.spacingS
visible: DMSNetworkService.profiles.length === 0
Item {
width: parent.width
height: DMSNetworkService.profiles.length === 0 ? 100 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "vpn_key_off"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Click Import to add a .ovpn or .conf")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
DankIcon {
name: "vpn_key_off"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
Repeater {
model: DMSNetworkService.profiles
StyledText {
text: I18n.tr("No VPN profiles")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
delegate: Rectangle {
id: profileRow
required property var modelData
required property int index
StyledText {
text: I18n.tr("Click Import to add a .ovpn or .conf")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
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
DankListView {
id: vpnListView
anchors.fill: parent
visible: DMSNetworkService.profiles.length > 0
spacing: 4
cacheBuffer: 200
clip: true
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
model: ScriptModel {
values: DMSNetworkService.profiles
objectProp: "uuid"
}
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(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 = "";
} else {
root.expandedUuid = modelData.uuid;
VPNService.getConfig(modelData.uuid);
}
}
}
}
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({
title: I18n.tr("Delete VPN"),
message: I18n.tr("Delete \"") + modelData.name + "\"?",
confirmText: I18n.tr("Delete"),
confirmColor: Theme.error,
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
}
}
delegate: VpnProfileDelegate {
required property var modelData
width: vpnListView.width
profile: modelData
isExpanded: root.expandedUuid === modelData.uuid
onToggleExpand: {
if (root.expandedUuid === modelData.uuid) {
root.expandedUuid = "";
return;
}
root.expandedUuid = modelData.uuid;
VPNService.getConfig(modelData.uuid);
}
onDeleteRequested: {
deleteConfirm.showWithOptions({
"title": I18n.tr("Delete VPN"),
"message": I18n.tr("Delete \"") + modelData.name + "\"?",
"confirmText": I18n.tr("Delete"),
"confirmColor": Theme.error,
"onConfirm": () => VPNService.deleteVpn(modelData.uuid)
});
}
}
}

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