mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 11:38:30 -04:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d91481c464 | |||
| dd7f4f3ac5 | |||
| 3df801e3b2 | |||
| 74d220767d | |||
| a3fba8fcb6 | |||
| 33384e4d5d | |||
| 78f2ea4642 | |||
| 7dffe85e94 | |||
| c5c3105469 | |||
| 8f100960cf | |||
| 8a5e7c78fb | |||
| 8a85ab06a1 | |||
| 30ceda97a0 | |||
| fefc0afa6f | |||
| b01042d530 | |||
| 7355cc54b2 | |||
| 187b7ba8ff | |||
| c0e7c1b2b4 | |||
| ea0c235d6e | |||
| 57a04cf409 | |||
| fd99558ce5 | |||
| 81a4d3b4e0 | |||
| bfda5f2d66 | |||
| 65c4167ab2 | |||
| e7bd8e4002 | |||
| 25a1b125b7 | |||
| d766c0773c | |||
| 5d12962fd6 | |||
| 9911698237 | |||
| 7e095415a7 | |||
| 68d5d7c5ed | |||
| 7b1c542585 | |||
| 52ed719489 | |||
| 6e508d416d | |||
| 518cc93276 | |||
| 8cb4e21716 | |||
| 19b44b1782 | |||
| 0509694d78 | |||
| da44e07cf7 | |||
| b34941e3b8 | |||
| c554d973ef | |||
| 2861cc89c6 | |||
| 1c5965f1e4 | |||
| 66d552b3d5 | |||
| ebad8e2dda | |||
| 8dadf73d52 | |||
| 9f77b3127c | |||
| 46f8457777 | |||
| 8465ed4311 | |||
| 6e3e178721 | |||
| 27230227ef | |||
| 7178563c2c | |||
| dfe309a543 | |||
| 04962ffed1 | |||
| 5008406be8 | |||
| 930a36bcfc | |||
| 3253adb6dd | |||
| b8fd52e00f | |||
| 8e7126472e | |||
| 61108b7668 | |||
| 09ca37c9fc | |||
| a2513e939a | |||
| a73380b310 | |||
| 69d7c1fb13 | |||
| d1eb655e46 | |||
| a3ecef7627 | |||
| 186621ab81 | |||
| 31efe9b385 | |||
| 6cdb891551 | |||
| 3bb3da228c | |||
| 0c99e4b8d1 | |||
| 8b79d1dad3 | |||
| 85fa3326c1 | |||
| 2fade1a617 | |||
| 597ba597e5 | |||
| 957020c9ea | |||
| ae520620ac | |||
| 4c43bed2f8 | |||
| 2663bcd96c | |||
| e423e17807 | |||
| 70fa0ec903 | |||
| db3615805c | |||
| 8162e60475 | |||
| 5594ec27ff | |||
| ca050ace9c | |||
| 093acdbff7 | |||
| a98417d66b | |||
| 83b9cdcb27 | |||
| 4ae8ef927c | |||
| 2128e6f14e | |||
| 9ff085c46a | |||
| 57d08f6b3b | |||
| 7964b66827 | |||
| 38a03a4485 | |||
| a1727d18fd | |||
| aabcef0c9c | |||
| 19406e99b9 | |||
| 5432d264f1 | |||
| d704a0ba3d | |||
| 161118122e | |||
| 9d550d7c11 | |||
| e40faaa442 | |||
| 894efa58a3 | |||
| 50570023ff | |||
| f293def3a8 | |||
| de3e8cffa0 | |||
| a874f76d72 | |||
| 424d4ed701 | |||
| 8f2a50c00e | |||
| 46cddb965f | |||
| 1ca64abad9 | |||
| 32d68a034f | |||
| 1a176920d4 | |||
| afa6fd9e2e | |||
| 6bee1b2c86 |
@@ -24,6 +24,7 @@ jobs:
|
|||||||
check-updates:
|
check-updates:
|
||||||
name: Check for updates
|
name: Check for updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ jobs:
|
|||||||
check-updates:
|
check-updates:
|
||||||
name: Check package/series updates
|
name: Check package/series updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
name: Void Linux XBPS Repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
force_rebuild:
|
||||||
|
description: 'Force rebuilding packages even if they already exist in the repository'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
build_git:
|
||||||
|
description: 'Build dms-git package'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
build_dms:
|
||||||
|
description: 'Build stable dms package'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
build_greeter:
|
||||||
|
description: 'Build stable dms-greeter package'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
name: Build & Deploy XBPS packages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell'
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Checkout gh-pages branch
|
||||||
|
run: |
|
||||||
|
git clone --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages-repo || {
|
||||||
|
echo "⚠️ gh-pages branch not found or empty, initializing a new one..."
|
||||||
|
mkdir gh-pages-repo
|
||||||
|
cd gh-pages-repo
|
||||||
|
git init
|
||||||
|
git checkout -b gh-pages
|
||||||
|
git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||||
|
cd ..
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Install XBPS Static Utilities
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ github.workspace }}/xbps
|
||||||
|
curl -s -L https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz | tar -xJ -C ${{ github.workspace }}/xbps
|
||||||
|
echo "${{ github.workspace }}/xbps/usr/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Clone void-packages
|
||||||
|
run: |
|
||||||
|
git clone --depth=1 https://github.com/void-linux/void-packages.git
|
||||||
|
|
||||||
|
- name: Inject templates
|
||||||
|
run: |
|
||||||
|
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
|
||||||
|
cp -R distro/void/srcpkgs/dms-greeter void-packages/srcpkgs/
|
||||||
|
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
|
||||||
|
|
||||||
|
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
|
||||||
|
run: |
|
||||||
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||||
|
|
||||||
|
- name: Bootstrap xbps-src
|
||||||
|
run: |
|
||||||
|
cd void-packages
|
||||||
|
./xbps-src binary-bootstrap
|
||||||
|
|
||||||
|
- name: Configure signing keys and trust
|
||||||
|
run: |
|
||||||
|
# Write private key; extract and register the public key
|
||||||
|
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
|
||||||
|
chmod 600 /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
# Extract public key in PEM format
|
||||||
|
openssl rsa -in /tmp/xbps_privkey.pem -pubout -out /tmp/dms-key.pub
|
||||||
|
rm -f /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
# Compute MD5 fingerprint in colon-separated hex format
|
||||||
|
FINGERPRINT=$(openssl rsa -pubin -in /tmp/dms-key.pub -outform DER 2>/dev/null | openssl dgst -md5 -c | tr '[:upper:]' '[:lower:]' | awk '{print $NF}')
|
||||||
|
|
||||||
|
# Format key in XML property list (plist) format as expected by xbps
|
||||||
|
mkdir -p /tmp/keys
|
||||||
|
cat <<EOF > "/tmp/keys/${FINGERPRINT}.plist"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>public-key</key>
|
||||||
|
<data>$(base64 -w0 /tmp/dms-key.pub)</data>
|
||||||
|
<key>public-key-size</key>
|
||||||
|
<integer>4096</integer>
|
||||||
|
<key>signature-by</key>
|
||||||
|
<string>AvengeMedia</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Copy keys to all host and chroot trust stores
|
||||||
|
sudo mkdir -p /var/db/xbps/keys
|
||||||
|
sudo cp "/tmp/keys/${FINGERPRINT}.plist" "/var/db/xbps/keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
mkdir -p void-packages/masterdir/var/db/xbps/keys
|
||||||
|
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/masterdir/var/db/xbps/keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
mkdir -p void-packages/etc/xbps.d/keys
|
||||||
|
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/etc/xbps.d/keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
mkdir -p void-packages/common/repo-keys
|
||||||
|
cp "/tmp/keys/${FINGERPRINT}.plist" "void-packages/common/repo-keys/${FINGERPRINT}.plist"
|
||||||
|
|
||||||
|
rm -rf /tmp/keys /tmp/dms-key.pub
|
||||||
|
|
||||||
|
- name: Configure repositories
|
||||||
|
run: |
|
||||||
|
# Append the repository to repos-remote templates so xbps-src translates it automatically
|
||||||
|
echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
|
||||||
|
echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
|
||||||
|
|
||||||
|
# Add any existing compiled packages to the build cache directory to avoid rebuilds
|
||||||
|
if [ -d "gh-pages-repo/current" ]; then
|
||||||
|
mkdir -p void-packages/hostdir/binpkgs
|
||||||
|
cp -L gh-pages-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true
|
||||||
|
xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: |
|
||||||
|
mkdir -p gh-pages-repo/current
|
||||||
|
|
||||||
|
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
|
||||||
|
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
|
||||||
|
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
|
||||||
|
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
|
BUILD_DMS="false"
|
||||||
|
BUILD_GREETER="false"
|
||||||
|
BUILD_GIT="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
|
BUILD_DMS="true"
|
||||||
|
BUILD_GREETER="true"
|
||||||
|
BUILD_GIT="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Starting Builds ==="
|
||||||
|
echo "DMS stable build enabled: $BUILD_DMS"
|
||||||
|
echo "Greeter stable build enabled: $BUILD_GREETER"
|
||||||
|
echo "Git build enabled: $BUILD_GIT"
|
||||||
|
echo "Force rebuild: $FORCE_REBUILD"
|
||||||
|
|
||||||
|
cd void-packages
|
||||||
|
|
||||||
|
# 1. Build dms-git (development package)
|
||||||
|
if [ "$BUILD_GIT" = "true" ]; then
|
||||||
|
# Calculate dynamic git version (tag.commits.hash)
|
||||||
|
GIT_VER=$(git -C .. describe --tags --always | sed 's/^v//; s/-/./g')
|
||||||
|
echo "🔨 Preparing dms-git version $GIT_VER"
|
||||||
|
|
||||||
|
# Stage source tarball in the xbps-src source cache.
|
||||||
|
# --transform adds a top-level directory so xbps-src can extract
|
||||||
|
# into $wrksrc (create_wrksrc=yes handles the rest).
|
||||||
|
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
|
||||||
|
mkdir -p "$SRC_CACHE"
|
||||||
|
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||||
|
--exclude=void-packages \
|
||||||
|
--exclude=gh-pages-repo \
|
||||||
|
--exclude=.git \
|
||||||
|
--exclude=danklinux \
|
||||||
|
-C .. .
|
||||||
|
|
||||||
|
CHECKSUM=$(sha256sum "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" | cut -d' ' -f1)
|
||||||
|
|
||||||
|
# Dynamically patch template version, checksum, and distfiles
|
||||||
|
sed -i "s/^version=.*/version=${GIT_VER}/" srcpkgs/dms-git/template
|
||||||
|
sed -i "s/^checksum=.*/checksum=${CHECKSUM}/" srcpkgs/dms-git/template
|
||||||
|
sed -i "s|^distfiles=.*|distfiles=\"dms-git-${GIT_VER}.tar.gz\"|" srcpkgs/dms-git/template
|
||||||
|
|
||||||
|
EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps"
|
||||||
|
|
||||||
|
if [ -f "../gh-pages-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
||||||
|
echo "✅ $EXPECTED_GIT_FILE already exists, skipping build."
|
||||||
|
else
|
||||||
|
echo "🔨 Compiling dms-git..."
|
||||||
|
./xbps-src pkg dms-git
|
||||||
|
rm -f "../gh-pages-repo/current/${EXPECTED_GIT_FILE}"
|
||||||
|
cp -L hostdir/binpkgs/dms-git-*.xbps ../gh-pages-repo/current/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Build stable dms package
|
||||||
|
if [ "$BUILD_DMS" = "true" ]; then
|
||||||
|
STABLE_VER=$(grep -E '^version=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
|
||||||
|
STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
|
||||||
|
|
||||||
|
EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps"
|
||||||
|
if [ -f "../gh-pages-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
||||||
|
echo "✅ $EXPECTED_DMS_FILE already exists, skipping build."
|
||||||
|
else
|
||||||
|
echo "🔨 Compiling dms ($STABLE_VER)..."
|
||||||
|
./xbps-src pkg dms
|
||||||
|
rm -f "../gh-pages-repo/current/${EXPECTED_DMS_FILE}"
|
||||||
|
cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../gh-pages-repo/current/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Build stable dms-greeter package
|
||||||
|
if [ "$BUILD_GREETER" = "true" ]; then
|
||||||
|
GREETER_VER=$(grep -E '^version=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
|
||||||
|
GREETER_REV=$(grep -E '^revision=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
|
||||||
|
|
||||||
|
EXPECTED_GREETER_FILE="dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps"
|
||||||
|
if [ -f "../gh-pages-repo/current/$EXPECTED_GREETER_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
||||||
|
echo "✅ $EXPECTED_GREETER_FILE already exists, skipping build."
|
||||||
|
else
|
||||||
|
echo "🔨 Compiling dms-greeter ($GREETER_VER)..."
|
||||||
|
./xbps-src pkg dms-greeter
|
||||||
|
rm -f "../gh-pages-repo/current/${EXPECTED_GREETER_FILE}"
|
||||||
|
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../gh-pages-repo/current/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Index and sign repository
|
||||||
|
run: |
|
||||||
|
cd gh-pages-repo/current
|
||||||
|
|
||||||
|
# Clean up any stale or dangling signature files to prevent O_CREAT ENOENT errors
|
||||||
|
rm -f *.sig2 *.sig
|
||||||
|
|
||||||
|
# Guard: nothing to index if no .xbps files exist
|
||||||
|
if ! ls *.xbps 1>/dev/null 2>&1; then
|
||||||
|
echo "⚠️ No .xbps files found to index, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Regenerate repo index
|
||||||
|
xbps-rindex -a $(pwd)/*.xbps
|
||||||
|
|
||||||
|
# Sign repository
|
||||||
|
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
|
||||||
|
chmod 600 /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
xbps-rindex --sign --signedby "AvengeMedia <AvengeMedia.US@gmail.com>" --privkey /tmp/xbps_privkey.pem $(pwd)
|
||||||
|
xbps-rindex --sign-pkg --privkey /tmp/xbps_privkey.pem $(pwd)/*.xbps
|
||||||
|
|
||||||
|
rm -f /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
|
- name: Deploy to gh-pages branch
|
||||||
|
run: |
|
||||||
|
cd gh-pages-repo
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add current/
|
||||||
|
git diff --quiet && git diff --staged --quiet || (git commit -m "Update XBPS packages [skip ci]" && git push origin gh-pages)
|
||||||
@@ -110,6 +110,9 @@ bin/
|
|||||||
# Core dumps
|
# Core dumps
|
||||||
core.*
|
core.*
|
||||||
|
|
||||||
|
# prek-installed local git hooks (generated from .pre-commit-config.yaml)
|
||||||
|
.githooks/
|
||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
|
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: i18n-term-freeze
|
||||||
|
name: i18n term freeze (no new I18n.tr/qsTr terms)
|
||||||
|
entry: python3 quickshell/translations/check_term_freeze.py
|
||||||
|
language: system
|
||||||
|
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
|
||||||
|
pass_filenames: false
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: no-console-in-qml
|
- id: no-console-in-qml
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ install-desktop:
|
|||||||
@echo "Installing desktop entries..."
|
@echo "Installing desktop entries..."
|
||||||
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
|
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/com.danklinux.dms.notepad.desktop $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
|
||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entries installed"
|
@echo "Desktop entries installed"
|
||||||
|
|
||||||
@@ -120,6 +121,7 @@ uninstall-desktop:
|
|||||||
@echo "Removing desktop entries..."
|
@echo "Removing desktop entries..."
|
||||||
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
|
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.desktop
|
||||||
|
@rm -f $(APPLICATIONS_DIR)/com.danklinux.dms.notepad.desktop
|
||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entries removed"
|
@echo "Desktop entries removed"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DMS Notepad
|
||||||
|
GenericName=Text Editor
|
||||||
|
Comment=Quick notes and text files in the dank desktop shell
|
||||||
|
Exec=dms ipc call notepad openFile %f
|
||||||
|
Icon=danklogo
|
||||||
|
Terminal=false
|
||||||
|
NoDisplay=true
|
||||||
|
Categories=Utility;TextEditor;
|
||||||
|
MimeType=text/plain;text/markdown;
|
||||||
|
StartupNotify=false
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.10.1
|
rev: v2.12.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ var clipPasteCmd = &cobra.Command{
|
|||||||
Run: runClipPaste,
|
Run: runClipPaste,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clipSendPasteCmd = &cobra.Command{
|
||||||
|
Use: "send-paste",
|
||||||
|
Short: "Send a paste keystroke to the focused window",
|
||||||
|
Long: "Emulate ctrl+v (or ctrl+shift+v with --shift) via a virtual keyboard. Works without server.",
|
||||||
|
Run: runClipSendPaste,
|
||||||
|
}
|
||||||
|
|
||||||
|
var clipSendPasteShift bool
|
||||||
|
|
||||||
var clipWatchCmd = &cobra.Command{
|
var clipWatchCmd = &cobra.Command{
|
||||||
Use: "watch [command]",
|
Use: "watch [command]",
|
||||||
Short: "Watch clipboard for changes",
|
Short: "Watch clipboard for changes",
|
||||||
@@ -216,8 +225,10 @@ func init() {
|
|||||||
|
|
||||||
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
||||||
|
|
||||||
|
clipSendPasteCmd.Flags().BoolVarP(&clipSendPasteShift, "shift", "s", false, "Send ctrl+shift+v (terminal paste)")
|
||||||
|
|
||||||
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd)
|
||||||
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipSendPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
@@ -314,6 +325,12 @@ func runClipPaste(cmd *cobra.Command, args []string) {
|
|||||||
os.Stdout.Write(data)
|
os.Stdout.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runClipSendPaste(cmd *cobra.Command, args []string) {
|
||||||
|
if err := clipboard.SendPasteKeystroke(clipSendPasteShift); err != nil {
|
||||||
|
log.Fatalf("send-paste: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runClipWatch(cmd *cobra.Command, args []string) {
|
func runClipWatch(cmd *cobra.Command, args []string) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ func init() {
|
|||||||
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
})
|
})
|
||||||
|
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
|
||||||
|
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugSrvCmd = &cobra.Command{
|
var debugSrvCmd = &cobra.Command{
|
||||||
@@ -184,10 +186,22 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pluginsUpdateCmd = &cobra.Command{
|
var pluginsUpdateCmd = &cobra.Command{
|
||||||
Use: "update <plugin-id>",
|
Use: "update [plugin-id]",
|
||||||
Short: "Update a plugin by ID",
|
Short: "Update a plugin by ID, or all plugins",
|
||||||
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
|
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). If --all or -a is specified, all installed plugins will be updated.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
updateAll, _ := cmd.Flags().GetBool("all")
|
||||||
|
if updateAll {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return fmt.Errorf("cannot specify plugin ID when using --all/-a")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("requires exactly 1 arg (plugin ID) or use --all/-a")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -195,6 +209,26 @@ var pluginsUpdateCmd = &cobra.Command{
|
|||||||
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
checkOnly, _ := cmd.Flags().GetBool("check")
|
||||||
|
updateAll, _ := cmd.Flags().GetBool("all")
|
||||||
|
if checkOnly {
|
||||||
|
if updateAll {
|
||||||
|
if err := checkAllPluginsCLI(); err != nil {
|
||||||
|
log.Fatalf("Error checking updates: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := checkPluginCLI(args[0]); err != nil {
|
||||||
|
log.Fatalf("Error checking update: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if updateAll {
|
||||||
|
if err := updateAllPluginsCLI(); err != nil {
|
||||||
|
log.Fatalf("Error updating plugins: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := updatePluginCLI(args[0]); err != nil {
|
if err := updatePluginCLI(args[0]); err != nil {
|
||||||
log.Fatalf("Error updating plugin: %v", err)
|
log.Fatalf("Error updating plugin: %v", err)
|
||||||
}
|
}
|
||||||
@@ -370,7 +404,11 @@ func listInstalledPlugins() error {
|
|||||||
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
|
fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames))
|
||||||
for _, id := range installedNames {
|
for _, id := range installedNames {
|
||||||
if plugin, ok := pluginMap[id]; ok {
|
if plugin, ok := pluginMap[id]; ok {
|
||||||
fmt.Printf(" %s\n", plugin.Name)
|
hasUpdateStr := ""
|
||||||
|
if hasUpdates, _, err := manager.HasUpdates(id, plugin); err == nil && hasUpdates {
|
||||||
|
hasUpdateStr = " (update available)"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s%s\n", plugin.Name, hasUpdateStr)
|
||||||
fmt.Printf(" ID: %s\n", plugin.ID)
|
fmt.Printf(" ID: %s\n", plugin.ID)
|
||||||
fmt.Printf(" Category: %s\n", plugin.Category)
|
fmt.Printf(" Category: %s\n", plugin.Category)
|
||||||
fmt.Printf(" Author: %s\n", plugin.Author)
|
fmt.Printf(" Author: %s\n", plugin.Author)
|
||||||
@@ -550,6 +588,160 @@ func updatePluginCLI(idOrName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateAllPluginsCLI() error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := manager.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list installed plugins: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, _ := registry.List()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for _, pluginID := range installed {
|
||||||
|
plugin := plugins.FindByIDOrName(pluginID, pluginList)
|
||||||
|
if plugin != nil {
|
||||||
|
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
if err := manager.Update(*plugin); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "cannot update system plugin") {
|
||||||
|
fmt.Printf("Skipping system plugin: %s\n", plugin.Name)
|
||||||
|
} else {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to update %s: %w", plugin.Name, err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Updating plugin: %s\n", pluginID)
|
||||||
|
if err := manager.UpdateByIDOrName(pluginID); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "cannot update system plugin") {
|
||||||
|
fmt.Printf("Skipping system plugin: %s\n", pluginID)
|
||||||
|
} else {
|
||||||
|
errs = append(errs, fmt.Errorf("failed to update %s: %w", pluginID, err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", pluginID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
for _, err := range errs {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to update some plugins")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPluginCLI(idOrName string) error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, _ := registry.List()
|
||||||
|
plugin := plugins.FindByIDOrName(idOrName, pluginList)
|
||||||
|
|
||||||
|
if plugin != nil {
|
||||||
|
installed, err := manager.IsInstalled(*plugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check install status: %w", err)
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUpdates, _, err := manager.HasUpdates(plugin.ID, *plugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasUpdates {
|
||||||
|
fmt.Printf("Update available for plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin is up to date: %s\n", plugin.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dummyPlugin := plugins.Plugin{ID: idOrName}
|
||||||
|
hasUpdates, _, err := manager.HasUpdates(idOrName, dummyPlugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasUpdates {
|
||||||
|
fmt.Printf("Update available for plugin: %s\n", idOrName)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Plugin is up to date: %s\n", idOrName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAllPluginsCLI() error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := manager.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list installed plugins: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, _ := registry.List()
|
||||||
|
|
||||||
|
var count int
|
||||||
|
for _, pluginID := range installed {
|
||||||
|
plugin := plugins.FindByIDOrName(pluginID, pluginList)
|
||||||
|
var hasUpdates bool
|
||||||
|
var name string
|
||||||
|
|
||||||
|
if plugin != nil {
|
||||||
|
name = plugin.Name
|
||||||
|
hasUpdates, _, _ = manager.HasUpdates(pluginID, *plugin)
|
||||||
|
} else {
|
||||||
|
name = pluginID
|
||||||
|
dummyPlugin := plugins.Plugin{ID: pluginID}
|
||||||
|
hasUpdates, _, _ = manager.HasUpdates(pluginID, dummyPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasUpdates {
|
||||||
|
fmt.Printf("Update available for plugin: %s (ID: %s)\n", name, pluginID)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
fmt.Printf("\nFound %d plugin(s) with available updates.\n", count)
|
||||||
|
} else {
|
||||||
|
fmt.Println("All plugins are up to date.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getCommonCommands() []*cobra.Command {
|
func getCommonCommands() []*cobra.Command {
|
||||||
return []*cobra.Command{
|
return []*cobra.Command{
|
||||||
versionCmd,
|
versionCmd,
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ var resolveIncludeCmd = &cobra.Command{
|
|||||||
"cursor.lua",
|
"cursor.lua",
|
||||||
"windowrules.lua",
|
"windowrules.lua",
|
||||||
"cursor.kdl",
|
"cursor.kdl",
|
||||||
|
"layout.kdl",
|
||||||
"outputs.kdl",
|
"outputs.kdl",
|
||||||
"binds.kdl",
|
"binds.kdl",
|
||||||
"cursor.conf",
|
"cursor.conf",
|
||||||
|
"layout.conf",
|
||||||
"outputs.conf",
|
"outputs.conf",
|
||||||
"binds.conf",
|
"binds.conf",
|
||||||
}, cobra.ShellCompDirectiveNoFileComp
|
}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ var greeterCmd = &cobra.Command{
|
|||||||
var (
|
var (
|
||||||
greeterConfigSyncFn = greeter.SyncDMSConfigs
|
greeterConfigSyncFn = greeter.SyncDMSConfigs
|
||||||
sharedAuthSyncFn = sharedpam.SyncAuthConfig
|
sharedAuthSyncFn = sharedpam.SyncAuthConfig
|
||||||
|
greeterIsNixOSFn = greeter.IsNixOS
|
||||||
)
|
)
|
||||||
|
|
||||||
var greeterInstallCmd = &cobra.Command{
|
var greeterInstallCmd = &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
Short: "Install and configure DMS greeter",
|
||||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: preRunGreeterMutation,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -64,6 +65,9 @@ var greeterSyncCmd = &cobra.Command{
|
|||||||
Short: "Sync DMS theme and settings with greeter",
|
Short: "Sync DMS theme and settings with greeter",
|
||||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.",
|
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.",
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := rejectNixOSGreeterMutation(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
profile, _ := cmd.Flags().GetBool("profile")
|
profile, _ := cmd.Flags().GetBool("profile")
|
||||||
if profile {
|
if profile {
|
||||||
return nil
|
return nil
|
||||||
@@ -140,7 +144,7 @@ var greeterEnableCmd = &cobra.Command{
|
|||||||
Use: "enable",
|
Use: "enable",
|
||||||
Short: "Enable DMS greeter in greetd config",
|
Short: "Enable DMS greeter in greetd config",
|
||||||
Long: "Configure greetd to use DMS as the greeter",
|
Long: "Configure greetd to use DMS as the greeter",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: preRunGreeterMutation,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -176,7 +180,7 @@ var greeterUninstallCmd = &cobra.Command{
|
|||||||
Use: "uninstall",
|
Use: "uninstall",
|
||||||
Short: "Remove DMS greeter configuration and restore previous display manager",
|
Short: "Remove DMS greeter configuration and restore previous display manager",
|
||||||
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
|
||||||
PreRunE: preRunPrivileged,
|
PreRunE: preRunGreeterMutation,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
yes, _ := cmd.Flags().GetBool("yes")
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
term, _ := cmd.Flags().GetBool("terminal")
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
@@ -206,6 +210,21 @@ func init() {
|
|||||||
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
|
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rejectNixOSGreeterMutation(cmd *cobra.Command) error {
|
||||||
|
if !greeterIsNixOSFn() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("dms %s is disabled on NixOS because the greeter is managed declaratively\nConfigure the DMS greeter in your NixOS module, then apply the change with your normal nixos-rebuild workflow", normalizeCommandSpec(cmd.CommandPath()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func preRunGreeterMutation(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := rejectNixOSGreeterMutation(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return preRunPrivileged(cmd, args)
|
||||||
|
}
|
||||||
|
|
||||||
func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error {
|
func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error {
|
||||||
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
|
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1415,24 +1434,36 @@ func readDefaultSessionCommand(configPath string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractGreeterCacheDirFromCommand(command string) string {
|
func explicitGreeterCacheDirFromCommand(command string) (string, bool) {
|
||||||
if command == "" {
|
|
||||||
return greeter.GreeterCacheDir
|
|
||||||
}
|
|
||||||
tokens := strings.Fields(command)
|
tokens := strings.Fields(command)
|
||||||
for i := 0; i < len(tokens); i++ {
|
for i := 0; i < len(tokens); i++ {
|
||||||
token := strings.Trim(tokens[i], "\"")
|
token := strings.Trim(tokens[i], "\"")
|
||||||
if token == "--cache-dir" && i+1 < len(tokens) {
|
if token == "--cache-dir" && i+1 < len(tokens) {
|
||||||
return strings.Trim(tokens[i+1], "\"")
|
value := strings.Trim(tokens[i+1], "\"")
|
||||||
|
if value != "" {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(token, "--cache-dir=") {
|
if strings.HasPrefix(token, "--cache-dir=") {
|
||||||
value := strings.TrimPrefix(token, "--cache-dir=")
|
value := strings.TrimPrefix(token, "--cache-dir=")
|
||||||
value = strings.Trim(value, "\"")
|
value = strings.Trim(value, "\"")
|
||||||
if value != "" {
|
if value != "" {
|
||||||
return value
|
return value, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
const nixOSGreeterStateDir = "/var/lib/dms-greeter"
|
||||||
|
|
||||||
|
func greeterStatusStateDir(command string, isNixOS bool) string {
|
||||||
|
if cacheDir, ok := explicitGreeterCacheDirFromCommand(command); ok {
|
||||||
|
return cacheDir
|
||||||
|
}
|
||||||
|
if isNixOS {
|
||||||
|
return nixOSGreeterStateDir
|
||||||
|
}
|
||||||
return greeter.GreeterCacheDir
|
return greeter.GreeterCacheDir
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1569,6 +1600,10 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Println("=== DMS Greeter Status ===")
|
fmt.Println("=== DMS Greeter Status ===")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
|
if greeterIsNixOSFn() {
|
||||||
|
return checkNixOSGreeterStatus()
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -1632,7 +1667,7 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
|
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := extractGreeterCacheDirFromCommand(configuredCommand)
|
cacheDir := greeterStatusStateDir(configuredCommand, false)
|
||||||
fmt.Println("\nGreeter Cache Directory:")
|
fmt.Println("\nGreeter Cache Directory:")
|
||||||
fmt.Printf(" Effective cache dir: %s\n", cacheDir)
|
fmt.Printf(" Effective cache dir: %s\n", cacheDir)
|
||||||
if cacheDir != greeter.GreeterCacheDir {
|
if cacheDir != greeter.GreeterCacheDir {
|
||||||
@@ -1895,6 +1930,85 @@ func checkGreeterStatus() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkNixOSGreeterStatus() error {
|
||||||
|
const configPath = "/etc/greetd/config.toml"
|
||||||
|
|
||||||
|
configuredCommand := readDefaultSessionCommand(configPath)
|
||||||
|
allGood := true
|
||||||
|
|
||||||
|
fmt.Println("Greeter Configuration:")
|
||||||
|
switch {
|
||||||
|
case strings.Contains(configuredCommand, "dms-greeter"):
|
||||||
|
fmt.Println(" ✓ DMS greeter command found")
|
||||||
|
if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" {
|
||||||
|
fmt.Printf(" Wrapper: %s\n", wrapper)
|
||||||
|
}
|
||||||
|
case configuredCommand != "":
|
||||||
|
fmt.Println(" ⚠ greetd default session does not reference dms-greeter")
|
||||||
|
allGood = false
|
||||||
|
default:
|
||||||
|
fmt.Printf(" ℹ No readable DMS command found in %s\n", configPath)
|
||||||
|
}
|
||||||
|
fmt.Println(" ℹ NixOS manages greeter configuration declaratively; apply changes through your NixOS module.")
|
||||||
|
|
||||||
|
stateDir := greeterStatusStateDir(configuredCommand, true)
|
||||||
|
fmt.Println("\nGreeter State Directory:")
|
||||||
|
fmt.Printf(" Effective state dir: %s\n", stateDir)
|
||||||
|
if stateDir == nixOSGreeterStateDir {
|
||||||
|
fmt.Println(" ✓ Using the NixOS module state path")
|
||||||
|
}
|
||||||
|
if stat, err := os.Stat(stateDir); err == nil && stat.IsDir() {
|
||||||
|
fmt.Printf(" ✓ %s exists\n", stateDir)
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
fmt.Printf(" ✗ %s not found\n", stateDir)
|
||||||
|
fmt.Println(" Rebuild your NixOS configuration after enabling the DMS greeter module.")
|
||||||
|
allGood = false
|
||||||
|
} else if err != nil {
|
||||||
|
fmt.Printf(" ✗ Could not inspect %s: %v\n", stateDir, err)
|
||||||
|
allGood = false
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✗ %s is not a directory\n", stateDir)
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nDeclarative Configuration Files:")
|
||||||
|
configFiles := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{name: "Settings", path: filepath.Join(stateDir, "settings.json")},
|
||||||
|
{name: "Session state", path: filepath.Join(stateDir, "session.json")},
|
||||||
|
{name: "Color theme", path: filepath.Join(stateDir, "colors.json")},
|
||||||
|
}
|
||||||
|
for _, configFile := range configFiles {
|
||||||
|
if stat, err := os.Stat(configFile.path); err == nil && !stat.IsDir() {
|
||||||
|
fmt.Printf(" ✓ %s: %s\n", configFile.name, configFile.path)
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
fmt.Printf(" ℹ %s not present (optional; configure configHome/configFiles in the NixOS module)\n", configFile.name)
|
||||||
|
} else if err != nil {
|
||||||
|
fmt.Printf(" ⚠ %s could not be inspected: %v\n", configFile.name, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ⚠ %s path is not a regular file: %s\n", configFile.name, configFile.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nGroup Membership:")
|
||||||
|
fmt.Println(" ℹ User group membership is managed by NixOS and is not required for declarative theme copies.")
|
||||||
|
|
||||||
|
fmt.Println("\nGreeter PAM Authentication:")
|
||||||
|
fmt.Println(" ℹ PAM is managed by NixOS modules.")
|
||||||
|
fmt.Println(" Configure fingerprint/U2F through security.pam.services.greetd.")
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
if allGood {
|
||||||
|
fmt.Println("✓ NixOS greeter state looks healthy and is managed declaratively.")
|
||||||
|
} else {
|
||||||
|
fmt.Println("⚠ Some issues detected. Update the DMS greeter module and rebuild NixOS; do not run 'dms greeter sync'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
|
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
|
||||||
if sampleLimit <= 0 {
|
if sampleLimit <= 0 {
|
||||||
sampleLimit = 3
|
sampleLimit = 3
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
||||||
@@ -85,3 +87,57 @@ func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
|||||||
t.Fatal("expected auth sync not to run after config sync failure")
|
t.Fatal("expected auth sync not to run after config sync failure")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGreeterStatusStateDirUsesNixOSDefault(t *testing.T) {
|
||||||
|
if got := greeterStatusStateDir("", true); got != nixOSGreeterStateDir {
|
||||||
|
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, nixOSGreeterStateDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) {
|
||||||
|
command := "dms-greeter --cache-dir /srv/dms-greeter --command niri"
|
||||||
|
if got := greeterStatusStateDir(command, true); got != "/srv/dms-greeter" {
|
||||||
|
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, "/srv/dms-greeter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) {
|
||||||
|
origGreeterIsNixOSFn := greeterIsNixOSFn
|
||||||
|
greeterIsNixOSFn = func() bool { return true }
|
||||||
|
t.Cleanup(func() {
|
||||||
|
greeterIsNixOSFn = origGreeterIsNixOSFn
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, commandName := range []string{"install", "enable", "sync", "uninstall"} {
|
||||||
|
t.Run(commandName, func(t *testing.T) {
|
||||||
|
root := &cobra.Command{Use: "dms"}
|
||||||
|
greeterCommand := &cobra.Command{Use: "greeter"}
|
||||||
|
mutationCommand := &cobra.Command{Use: commandName}
|
||||||
|
root.AddCommand(greeterCommand)
|
||||||
|
greeterCommand.AddCommand(mutationCommand)
|
||||||
|
|
||||||
|
err := rejectNixOSGreeterMutation(mutationCommand)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected NixOS greeter %s to be rejected", commandName)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "dms greeter "+commandName+" is disabled on NixOS") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "/var/cache/dms-greeter") {
|
||||||
|
t.Fatalf("NixOS remediation should not recommend the non-NixOS cache path: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRejectNixOSGreeterMutationAllowsOtherDistros(t *testing.T) {
|
||||||
|
origGreeterIsNixOSFn := greeterIsNixOSFn
|
||||||
|
greeterIsNixOSFn = func() bool { return false }
|
||||||
|
t.Cleanup(func() {
|
||||||
|
greeterIsNixOSFn = origGreeterIsNixOSFn
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := rejectNixOSGreeterMutation(&cobra.Command{Use: "sync"}); err != nil {
|
||||||
|
t.Fatalf("expected non-NixOS greeter command to be allowed, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,10 +38,17 @@ var matugenCheckCmd = &cobra.Command{
|
|||||||
Run: runMatugenCheck,
|
Run: runMatugenCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var matugenPreviewCmd = &cobra.Command{
|
||||||
|
Use: "preview",
|
||||||
|
Short: "Preview Matugen scheme colors without applying them",
|
||||||
|
Run: runMatugenPreview,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
matugenCmd.AddCommand(matugenGenerateCmd)
|
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||||
matugenCmd.AddCommand(matugenQueueCmd)
|
matugenCmd.AddCommand(matugenQueueCmd)
|
||||||
matugenCmd.AddCommand(matugenCheckCmd)
|
matugenCmd.AddCommand(matugenCheckCmd)
|
||||||
|
matugenCmd.AddCommand(matugenPreviewCmd)
|
||||||
|
|
||||||
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
||||||
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
||||||
@@ -62,6 +69,8 @@ func init() {
|
|||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
||||||
|
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
|
||||||
|
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
@@ -200,3 +209,17 @@ func runMatugenCheck(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runMatugenPreview(cmd *cobra.Command, args []string) {
|
||||||
|
sourceColor, _ := cmd.Flags().GetString("source-color")
|
||||||
|
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||||
|
previews, err := matugen.PreviewSchemes(sourceColor, contrast)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to generate Matugen previews: %v", err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(previews)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal Matugen previews: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultImmutablePolicyAllowsSyncButBlocksEnable(t *testing.T) {
|
||||||
|
var policyFile cliPolicyFile
|
||||||
|
if err := json.Unmarshal(defaultCLIPolicyJSON, &policyFile); err != nil {
|
||||||
|
t.Fatalf("failed to parse embedded CLI policy: %v", err)
|
||||||
|
}
|
||||||
|
if policyFile.BlockedCommands == nil {
|
||||||
|
t.Fatal("embedded CLI policy has no blocked_commands")
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked := normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||||
|
if !commandBlockedByPolicy("greeter enable", blocked) {
|
||||||
|
t.Fatal("expected greeter enable to remain blocked on immutable/image-based systems")
|
||||||
|
}
|
||||||
|
if commandBlockedByPolicy("greeter sync", blocked) {
|
||||||
|
t.Fatal("expected greeter sync to remain available on immutable/image-based systems")
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -22,7 +22,6 @@ require (
|
|||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
|
||||||
golang.org/x/image v0.39.0
|
golang.org/x/image v0.39.0
|
||||||
tailscale.com v1.96.5
|
tailscale.com v1.96.5
|
||||||
)
|
)
|
||||||
@@ -64,6 +63,7 @@ require (
|
|||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||||
golang.org/x/net v0.53.0 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const envServe = "_DMS_CLIPBOARD_SERVE"
|
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||||
@@ -17,6 +16,37 @@ const envMime = "_DMS_CLIPBOARD_MIME"
|
|||||||
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||||
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||||
|
|
||||||
|
type Offer struct {
|
||||||
|
MimeType string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// textMimeAliases are offered alongside plain-text content so legacy X11
|
||||||
|
// clients bridged through XWayland find a target they can convert.
|
||||||
|
var textMimeAliases = []string{
|
||||||
|
"text/plain",
|
||||||
|
"text/plain;charset=utf-8",
|
||||||
|
"UTF8_STRING",
|
||||||
|
"STRING",
|
||||||
|
"TEXT",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandOffers turns raw clipboard data into the full offer list to serve,
|
||||||
|
// adding the standard alias set for text content.
|
||||||
|
func ExpandOffers(data []byte, mimeType string) []Offer {
|
||||||
|
offers := []Offer{{MimeType: mimeType, Data: data}}
|
||||||
|
if mimeType != "text/plain" && mimeType != "text/plain;charset=utf-8" {
|
||||||
|
return offers
|
||||||
|
}
|
||||||
|
for _, alias := range textMimeAliases {
|
||||||
|
if alias == mimeType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
offers = append(offers, Offer{MimeType: alias, Data: data})
|
||||||
|
}
|
||||||
|
return offers
|
||||||
|
}
|
||||||
|
|
||||||
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||||
// child. Reads source data into memory, deletes any cache file, then serves.
|
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||||
func MaybeServeAndExit() {
|
func MaybeServeAndExit() {
|
||||||
@@ -44,7 +74,7 @@ func MaybeServeAndExit() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
if err := serveOffers(ExpandOffers(data, mimeType), pasteOnce); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -55,22 +85,33 @@ func Copy(data []byte, mimeType string) error {
|
|||||||
return copyForkCached(data, mimeType, false)
|
return copyForkCached(data, mimeType, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CopyText(text string) error {
|
||||||
|
return Copy([]byte(text), "text/plain;charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if foreground {
|
||||||
return serveClipboard(data, mimeType, pasteOnce)
|
return serveOffers(ExpandOffers(data, mimeType), pasteOnce)
|
||||||
}
|
}
|
||||||
return copyForkCached(data, mimeType, pasteOnce)
|
return copyForkCached(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if !foreground {
|
||||||
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
buf, err := io.ReadAll(data)
|
buf, err := io.ReadAll(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read source: %w", err)
|
return fmt.Errorf("read source: %w", err)
|
||||||
}
|
}
|
||||||
return serveClipboard(buf, mimeType, pasteOnce)
|
return serveOffers(ExpandOffers(buf, mimeType), pasteOnce)
|
||||||
}
|
}
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
|
||||||
|
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
||||||
|
if foreground {
|
||||||
|
return serveOffers(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
return copyMultiFork(offers, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
@@ -132,12 +173,11 @@ func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
cmd := newForkCmd(mimeType, pasteOnce)
|
cmd := newForkCmd(mimeType, pasteOnce)
|
||||||
|
|
||||||
switch src := data.(type) {
|
if src, ok := data.(*os.File); ok {
|
||||||
case *os.File:
|
|
||||||
cmd.Stdin = src
|
cmd.Stdin = src
|
||||||
return waitReady(cmd)
|
return waitReady(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
@@ -165,6 +205,38 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
||||||
|
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||||
|
if pasteOnce {
|
||||||
|
args = append(args, "--paste-once")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, offer := range offers {
|
||||||
|
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
||||||
|
if _, err := stdin.Write(offer.Data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write offer data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func signalReady() {
|
||||||
@@ -194,57 +266,25 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
// serveOffers owns the Wayland selection until cancelled (or first paste when
|
||||||
display, err := wlclient.Connect("")
|
// pasteOnce is set), answering every offered mime type with its data.
|
||||||
|
func serveOffers(offers []Offer, pasteOnce bool) error {
|
||||||
|
if len(offers) == 0 {
|
||||||
|
return fmt.Errorf("no offers to serve")
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := connectSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
defer display.Destroy()
|
defer s.Close()
|
||||||
|
|
||||||
ctx := display.Context()
|
dataControlMgr, err := s.requireDataControl()
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get registry: %w", err)
|
return err
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
if seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
device, err := dataControlMgr.GetDataDevice(s.seat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get data device: %w", err)
|
return fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -255,25 +295,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("create data source: %w", err)
|
return fmt.Errorf("create data source: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := source.Offer(mimeType); err != nil {
|
offerData := make(map[string][]byte, len(offers))
|
||||||
return fmt.Errorf("offer mime type: %w", err)
|
for _, offer := range offers {
|
||||||
}
|
if err := source.Offer(offer.MimeType); err != nil {
|
||||||
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
|
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
||||||
if err := source.Offer("text/plain"); err != nil {
|
|
||||||
return fmt.Errorf("offer text/plain: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
|
|
||||||
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("UTF8_STRING"); err != nil {
|
|
||||||
return fmt.Errorf("offer UTF8_STRING: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("STRING"); err != nil {
|
|
||||||
return fmt.Errorf("offer STRING: %w", err)
|
|
||||||
}
|
|
||||||
if err := source.Offer("TEXT"); err != nil {
|
|
||||||
return fmt.Errorf("offer TEXT: %w", err)
|
|
||||||
}
|
}
|
||||||
|
offerData[offer.MimeType] = offer.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
@@ -283,7 +310,11 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
_ = syscall.SetNonblock(e.Fd, false)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
|
if data, ok := offerData[e.MimeType]; ok {
|
||||||
_, _ = file.Write(data)
|
_, _ = file.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -298,7 +329,7 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return fmt.Errorf("set selection: %w", err)
|
return fmt.Errorf("set selection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
signalReady()
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -310,70 +341,26 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := s.ctx.Dispatch(); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyText(text string) error {
|
|
||||||
return Copy([]byte(text), "text/plain;charset=utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Paste() ([]byte, string, error) {
|
func Paste() ([]byte, string, error) {
|
||||||
display, err := wlclient.Connect("")
|
s, err := connectSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
return nil, "", err
|
||||||
}
|
}
|
||||||
defer display.Destroy()
|
defer s.Close()
|
||||||
|
|
||||||
ctx := display.Context()
|
dataControlMgr, err := s.requireDataControl()
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("get registry: %w", err)
|
return nil, "", err
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
device, err := dataControlMgr.GetDataDevice(s.seat)
|
||||||
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
|
|
||||||
if seat == nil {
|
|
||||||
return nil, "", fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
return nil, "", fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -399,15 +386,14 @@ func Paste() ([]byte, string, error) {
|
|||||||
gotSelection = true
|
gotSelection = true
|
||||||
})
|
})
|
||||||
|
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
|
|
||||||
if !gotSelection || selectionOffer == nil {
|
if !gotSelection || selectionOffer == nil {
|
||||||
return nil, "", fmt.Errorf("no clipboard data")
|
return nil, "", fmt.Errorf("no clipboard data")
|
||||||
}
|
}
|
||||||
|
|
||||||
mimeTypes := offerMimeTypes[selectionOffer]
|
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer])
|
||||||
selectedMime := selectPreferredMimeType(mimeTypes)
|
|
||||||
if selectedMime == "" {
|
if selectedMime == "" {
|
||||||
return nil, "", fmt.Errorf("no supported mime type")
|
return nil, "", fmt.Errorf("no supported mime type")
|
||||||
}
|
}
|
||||||
@@ -424,7 +410,7 @@ func Paste() ([]byte, string, error) {
|
|||||||
}
|
}
|
||||||
w.Close()
|
w.Close()
|
||||||
|
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
data, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -470,161 +456,3 @@ func selectPreferredMimeType(mimes []string) string {
|
|||||||
func IsImageMimeType(mime string) bool {
|
func IsImageMimeType(mime string) bool {
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
return len(mime) > 6 && mime[:6] == "image/"
|
||||||
}
|
}
|
||||||
|
|
||||||
type Offer struct {
|
|
||||||
MimeType string
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
|
||||||
if !foreground {
|
|
||||||
return copyMultiFork(offers, pasteOnce)
|
|
||||||
}
|
|
||||||
return copyMultiServe(offers, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, offer := range offers {
|
|
||||||
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
|
||||||
if _, err := stdin.Write(offer.Data); err != nil {
|
|
||||||
stdin.Close()
|
|
||||||
return fmt.Errorf("write offer data: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stdin.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
ctx := display.Context()
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get registry: %w", err)
|
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(ctx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
if seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get data device: %w", err)
|
|
||||||
}
|
|
||||||
defer device.Destroy()
|
|
||||||
|
|
||||||
source, err := dataControlMgr.CreateDataSource()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create data source: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
offerMap := make(map[string][]byte)
|
|
||||||
for _, offer := range offers {
|
|
||||||
if err := source.Offer(offer.MimeType); err != nil {
|
|
||||||
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
|
||||||
}
|
|
||||||
offerMap[offer.MimeType] = offer.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelled := make(chan struct{})
|
|
||||||
pasted := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
|
||||||
_ = syscall.SetNonblock(e.Fd, false)
|
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
|
||||||
_, _ = file.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case pasted <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
|
||||||
close(cancelled)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := device.SetSelection(source); err != nil {
|
|
||||||
return fmt.Errorf("set selection: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-cancelled:
|
|
||||||
return nil
|
|
||||||
case <-pasted:
|
|
||||||
if pasteOnce {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if err := ctx.Dispatch(); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xkbKeymapFormatV1 = 1
|
||||||
|
|
||||||
|
keyStateReleased = 0
|
||||||
|
keyStatePressed = 1
|
||||||
|
|
||||||
|
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
|
||||||
|
shiftModMask = 1 << 0
|
||||||
|
ctrlModMask = 1 << 2
|
||||||
|
|
||||||
|
// evdev fallbacks for a standard pc105 map
|
||||||
|
fallbackCtrlKey = 29 // KEY_LEFTCTRL
|
||||||
|
fallbackShiftKey = 42 // KEY_LEFTSHIFT
|
||||||
|
fallbackVKey = 47 // KEY_V
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
|
||||||
|
// using the seat's own keymap, so keycodes stay valid for XWayland clients
|
||||||
|
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
|
||||||
|
// selects ctrl+shift+v for terminal targets.
|
||||||
|
func SendPasteKeystroke(withShift bool) error {
|
||||||
|
s, err := connectSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if s.virtualKeyboardMgr == nil {
|
||||||
|
return fmt.Errorf("compositor does not support zwp_virtual_keyboard_manager_v1")
|
||||||
|
}
|
||||||
|
if s.seat == nil {
|
||||||
|
return fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard, err := s.seat.GetKeyboard()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get keyboard: %w", err)
|
||||||
|
}
|
||||||
|
defer keyboard.Release()
|
||||||
|
|
||||||
|
var keymap *wlclient.KeyboardKeymapEvent
|
||||||
|
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||||
|
if keymap == nil {
|
||||||
|
keymap = &e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.display.Roundtrip()
|
||||||
|
|
||||||
|
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
|
||||||
|
return fmt.Errorf("no xkb keymap from seat")
|
||||||
|
}
|
||||||
|
defer unix.Close(keymap.Fd)
|
||||||
|
|
||||||
|
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read keymap: %w", err)
|
||||||
|
}
|
||||||
|
keys := resolveKeycodes(keymapText)
|
||||||
|
|
||||||
|
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create virtual keyboard: %w", err)
|
||||||
|
}
|
||||||
|
defer vk.Destroy()
|
||||||
|
|
||||||
|
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
|
||||||
|
return fmt.Errorf("set keymap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mods := uint32(ctrlModMask)
|
||||||
|
held := []uint32{keys.ctrl}
|
||||||
|
if withShift {
|
||||||
|
mods |= shiftModMask
|
||||||
|
held = append(held, keys.shift)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := uint32(0)
|
||||||
|
press := func(key, state uint32) error {
|
||||||
|
t++
|
||||||
|
return vk.Key(t, key, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range held {
|
||||||
|
if err := press(key, keyStatePressed); err != nil {
|
||||||
|
return fmt.Errorf("key press: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
|
||||||
|
return fmt.Errorf("set modifiers: %w", err)
|
||||||
|
}
|
||||||
|
if err := press(keys.v, keyStatePressed); err != nil {
|
||||||
|
return fmt.Errorf("key press: %w", err)
|
||||||
|
}
|
||||||
|
if err := press(keys.v, keyStateReleased); err != nil {
|
||||||
|
return fmt.Errorf("key release: %w", err)
|
||||||
|
}
|
||||||
|
for i := len(held) - 1; i >= 0; i-- {
|
||||||
|
if err := press(held[i], keyStateReleased); err != nil {
|
||||||
|
return fmt.Errorf("key release: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := vk.Modifiers(0, 0, 0, 0); err != nil {
|
||||||
|
return fmt.Errorf("clear modifiers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.display.Roundtrip()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKeymap(fd int, size uint32) (string, error) {
|
||||||
|
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
text := strings.TrimRight(string(data), "\x00")
|
||||||
|
return text, unix.Munmap(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pasteKeycodes struct {
|
||||||
|
ctrl uint32
|
||||||
|
shift uint32
|
||||||
|
v uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
|
||||||
|
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
|
||||||
|
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
|
||||||
|
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// xkbcommon may serialize keysyms as hex escapes instead of names
|
||||||
|
// (e.g. "0x76" for v, "0xffe3" for Control_L).
|
||||||
|
var keysymNames = map[uint32]string{
|
||||||
|
0x76: "v",
|
||||||
|
0xffe3: "Control_L",
|
||||||
|
0xffe1: "Shift_L",
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalKeysym(sym string) string {
|
||||||
|
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
|
||||||
|
return sym
|
||||||
|
}
|
||||||
|
value, err := strconv.ParseUint(sym[2:], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return sym
|
||||||
|
}
|
||||||
|
if name, ok := keysymNames[uint32(value)]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return sym
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
|
||||||
|
// the seat keymap's first group, falling back to pc105 positions.
|
||||||
|
func resolveKeycodes(keymap string) pasteKeycodes {
|
||||||
|
codes := map[string]uint32{}
|
||||||
|
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
|
||||||
|
if code, err := strconv.Atoi(m[2]); err == nil {
|
||||||
|
codes[m[1]] = uint32(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
|
||||||
|
want := map[string]*uint32{
|
||||||
|
"Control_L": &keys.ctrl,
|
||||||
|
"Shift_L": &keys.shift,
|
||||||
|
"v": &keys.v,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
|
||||||
|
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
|
||||||
|
if group == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xkbCode, ok := codes[m[1]]
|
||||||
|
if !ok || xkbCode < 8 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
|
||||||
|
target, wanted := want[level1]
|
||||||
|
if !wanted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*target = xkbCode - 8
|
||||||
|
delete(want, level1)
|
||||||
|
if len(want) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLiveSeatKeymapResolution(t *testing.T) {
|
||||||
|
if os.Getenv("DMS_LIVE_TEST") == "" {
|
||||||
|
t.Skip("set DMS_LIVE_TEST=1 to run against the live compositor")
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := connectSession()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("connect: %v", err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
if s.virtualKeyboardMgr == nil {
|
||||||
|
t.Fatal("compositor does not advertise zwp_virtual_keyboard_manager_v1")
|
||||||
|
}
|
||||||
|
if s.seat == nil {
|
||||||
|
t.Fatal("no seat")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboard, err := s.seat.GetKeyboard()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get keyboard: %v", err)
|
||||||
|
}
|
||||||
|
defer keyboard.Release()
|
||||||
|
|
||||||
|
var keymap *wlclient.KeyboardKeymapEvent
|
||||||
|
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||||
|
if keymap == nil {
|
||||||
|
keymap = &e
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.display.Roundtrip()
|
||||||
|
|
||||||
|
if keymap == nil {
|
||||||
|
t.Fatal("no keymap event")
|
||||||
|
}
|
||||||
|
defer unix.Close(keymap.Fd)
|
||||||
|
|
||||||
|
text, err := readKeymap(keymap.Fd, keymap.Size)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read keymap: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dump := os.Getenv("DMS_LIVE_DUMP"); dump != "" {
|
||||||
|
if err := os.WriteFile(dump, []byte(text), 0o644); err != nil {
|
||||||
|
t.Fatalf("dump keymap: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := resolveKeycodes(text)
|
||||||
|
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
|
||||||
|
|
||||||
|
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
|
||||||
|
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
const qwertyKeymap = `xkb_keymap {
|
||||||
|
xkb_keycodes "(unnamed)" {
|
||||||
|
minimum = 8;
|
||||||
|
maximum = 708;
|
||||||
|
<ESC> = 9;
|
||||||
|
<AB04> = 55;
|
||||||
|
<LCTL> = 37;
|
||||||
|
<LFSH> = 50;
|
||||||
|
alias <AL01> = <AC01>;
|
||||||
|
indicator 1 = "Caps Lock";
|
||||||
|
};
|
||||||
|
xkb_types "(unnamed)" {
|
||||||
|
type "ALPHABETIC" {
|
||||||
|
modifiers = Shift+Lock;
|
||||||
|
map[Shift] = Level2;
|
||||||
|
level_name[Level1] = "Base";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
xkb_symbols "(unnamed)" {
|
||||||
|
key <ESC> { [ Escape ] };
|
||||||
|
key <AB04> { type= "ALPHABETIC", [ v, V ] };
|
||||||
|
key <LCTL> { [ Control_L ] };
|
||||||
|
key <LFSH> { [ Shift_L ] };
|
||||||
|
};
|
||||||
|
};`
|
||||||
|
|
||||||
|
const hexKeymap = `xkb_keymap {
|
||||||
|
xkb_keycodes "(unnamed)" {
|
||||||
|
<AB04> = 56;
|
||||||
|
<LCTL> = 38;
|
||||||
|
<LFSH> = 51;
|
||||||
|
};
|
||||||
|
xkb_symbols "(unnamed)" {
|
||||||
|
key <LCTL> { [ 0xffe3 ] };
|
||||||
|
key <LFSH> {
|
||||||
|
type= "PC_ALT_LEVEL2",
|
||||||
|
symbols[1]= [ 0xffe1, 0xfe08 ]
|
||||||
|
};
|
||||||
|
key <AB04> { [ 0x76, 0x56 ] };
|
||||||
|
};
|
||||||
|
};`
|
||||||
|
|
||||||
|
const dvorakKeymap = `xkb_keymap {
|
||||||
|
xkb_keycodes "(unnamed)" {
|
||||||
|
<AB09> = 60;
|
||||||
|
<LCTL> = 37;
|
||||||
|
<LFSH> = 50;
|
||||||
|
};
|
||||||
|
xkb_symbols "(unnamed)" {
|
||||||
|
key <AB09> { [ v, V ] };
|
||||||
|
key <LCTL> { [ Control_L ] };
|
||||||
|
key <LFSH> { [ Shift_L ] };
|
||||||
|
};
|
||||||
|
};`
|
||||||
|
|
||||||
|
func TestResolveKeycodes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
keymap string
|
||||||
|
want pasteKeycodes
|
||||||
|
}{
|
||||||
|
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
|
||||||
|
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
|
||||||
|
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
|
||||||
|
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := resolveKeycodes(tt.keymap)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandOffers(t *testing.T) {
|
||||||
|
text := ExpandOffers([]byte("hi"), "text/plain;charset=utf-8")
|
||||||
|
if len(text) != 5 {
|
||||||
|
t.Fatalf("expected 5 text offers, got %d", len(text))
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, o := range text {
|
||||||
|
if string(o.Data) != "hi" {
|
||||||
|
t.Errorf("offer %s has wrong data", o.MimeType)
|
||||||
|
}
|
||||||
|
if seen[o.MimeType] {
|
||||||
|
t.Errorf("duplicate offer %s", o.MimeType)
|
||||||
|
}
|
||||||
|
seen[o.MimeType] = true
|
||||||
|
}
|
||||||
|
if !seen["UTF8_STRING"] || !seen["STRING"] || !seen["TEXT"] || !seen["text/plain"] {
|
||||||
|
t.Errorf("missing X11 alias offers: %v", seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
img := ExpandOffers([]byte{1}, "image/png")
|
||||||
|
if len(img) != 1 || img[0].MimeType != "image/png" {
|
||||||
|
t.Errorf("non-text mime should not expand, got %+v", img)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type session struct {
|
||||||
|
display *wlclient.Display
|
||||||
|
ctx *wlclient.Context
|
||||||
|
registry *wlclient.Registry
|
||||||
|
seat *wlclient.Seat
|
||||||
|
dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||||
|
virtualKeyboardMgr *virtual_keyboard.ZwpVirtualKeyboardManagerV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectSession opens a short-lived Wayland connection and binds the seat
|
||||||
|
// plus whichever clipboard-related globals the compositor advertises.
|
||||||
|
func connectSession() (*session, error) {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &session{display: display, ctx: display.Context()}
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
display.Destroy()
|
||||||
|
return nil, fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
s.registry = registry
|
||||||
|
|
||||||
|
var bindErr error
|
||||||
|
bind := func(name uint32, iface string, version uint32, proxy wlclient.Proxy) {
|
||||||
|
if err := registry.Bind(name, iface, version, proxy); err != nil {
|
||||||
|
bindErr = fmt.Errorf("bind %s: %w", iface, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case ext_data_control.ExtDataControlManagerV1InterfaceName:
|
||||||
|
mgr := ext_data_control.NewExtDataControlManagerV1(s.ctx)
|
||||||
|
bind(e.Name, e.Interface, e.Version, mgr)
|
||||||
|
s.dataControlMgr = mgr
|
||||||
|
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
|
||||||
|
mgr := virtual_keyboard.NewZwpVirtualKeyboardManagerV1(s.ctx)
|
||||||
|
bind(e.Name, e.Interface, e.Version, mgr)
|
||||||
|
s.virtualKeyboardMgr = mgr
|
||||||
|
case "wl_seat":
|
||||||
|
if s.seat != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seat := wlclient.NewSeat(s.ctx)
|
||||||
|
bind(e.Name, e.Interface, e.Version, seat)
|
||||||
|
s.seat = seat
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if bindErr != nil {
|
||||||
|
s.Close()
|
||||||
|
return nil, bindErr
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) requireDataControl() (*ext_data_control.ExtDataControlManagerV1, error) {
|
||||||
|
switch {
|
||||||
|
case s.dataControlMgr == nil:
|
||||||
|
return nil, fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
|
case s.seat == nil:
|
||||||
|
return nil, fmt.Errorf("no seat available")
|
||||||
|
default:
|
||||||
|
return s.dataControlMgr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *session) Close() {
|
||||||
|
if s.dataControlMgr != nil {
|
||||||
|
s.dataControlMgr.Destroy()
|
||||||
|
}
|
||||||
|
if s.registry != nil {
|
||||||
|
s.registry.Destroy()
|
||||||
|
}
|
||||||
|
s.display.Destroy()
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ hl.config({
|
|||||||
input = {
|
input = {
|
||||||
kb_layout = "us",
|
kb_layout = "us",
|
||||||
numlock_by_default = true,
|
numlock_by_default = true,
|
||||||
|
follow_mouse = 0,
|
||||||
touchpad = {
|
touchpad = {
|
||||||
tap_to_click = true,
|
tap_to_click = true,
|
||||||
natural_scroll = true,
|
natural_scroll = true,
|
||||||
|
|||||||
@@ -19,6 +19,20 @@ type NiriProvider struct {
|
|||||||
parsed bool
|
parsed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type niriActionPart struct {
|
||||||
|
value string
|
||||||
|
quoted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var niriActionPropertyOrder = []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"}
|
||||||
|
var niriActionProperties = map[string]struct{}{
|
||||||
|
"focus": {},
|
||||||
|
"show-pointer": {},
|
||||||
|
"write-to-disk": {},
|
||||||
|
"skip-confirmation": {},
|
||||||
|
"delay-ms": {},
|
||||||
|
}
|
||||||
|
|
||||||
func NewNiriProvider(configDir string) *NiriProvider {
|
func NewNiriProvider(configDir string) *NiriProvider {
|
||||||
if configDir == "" {
|
if configDir == "" {
|
||||||
configDir = defaultNiriConfigDir()
|
configDir = defaultNiriConfigDir()
|
||||||
@@ -55,6 +69,7 @@ func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
sheet := &keybinds.CheatSheet{
|
sheet := &keybinds.CheatSheet{
|
||||||
Title: "Niri Keybinds",
|
Title: "Niri Keybinds",
|
||||||
Provider: n.Name(),
|
Provider: n.Name(),
|
||||||
|
ModKey: result.ModKey,
|
||||||
Binds: categorizedBinds,
|
Binds: categorizedBinds,
|
||||||
DMSBindsIncluded: result.DMSBindsIncluded,
|
DMSBindsIncluded: result.DMSBindsIncluded,
|
||||||
}
|
}
|
||||||
@@ -352,7 +367,7 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if actionNode.Properties != nil {
|
if actionNode.Properties != nil {
|
||||||
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
|
for _, propName := range niriActionPropertyOrder {
|
||||||
if val, ok := actionNode.Properties.Get(propName); ok {
|
if val, ok := actionNode.Properties.Get(propName); ok {
|
||||||
parts = append(parts, propName+"="+val.String())
|
parts = append(parts, propName+"="+val.String())
|
||||||
}
|
}
|
||||||
@@ -440,10 +455,10 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
node.SetName(parts[0])
|
node.SetName(parts[0].value)
|
||||||
for _, arg := range parts[1:] {
|
for _, arg := range parts[1:] {
|
||||||
if strings.Contains(arg, "=") {
|
if n.isNiriActionPropertyToken(arg) {
|
||||||
kv := strings.SplitN(arg, "=", 2)
|
kv := strings.SplitN(arg.value, "=", 2)
|
||||||
switch kv[1] {
|
switch kv[1] {
|
||||||
case "true":
|
case "true":
|
||||||
node.AddProperty(kv[0], true, "")
|
node.AddProperty(kv[0], true, "")
|
||||||
@@ -454,13 +469,25 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
node.AddArgument(arg, "")
|
node.AddArgument(arg.value, "")
|
||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) parseActionParts(action string) []string {
|
func (n *NiriProvider) isNiriActionPropertyToken(part niriActionPart) bool {
|
||||||
var parts []string
|
if part.quoted || !strings.Contains(part.value, "=") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
key, _, ok := strings.Cut(part.value, "=")
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok = niriActionProperties[key]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) parseActionParts(action string) []niriActionPart {
|
||||||
|
var parts []niriActionPart
|
||||||
var current strings.Builder
|
var current strings.Builder
|
||||||
var inQuote, escaped, wasQuoted bool
|
var inQuote, escaped, wasQuoted bool
|
||||||
|
|
||||||
@@ -476,7 +503,7 @@ func (n *NiriProvider) parseActionParts(action string) []string {
|
|||||||
inQuote = !inQuote
|
inQuote = !inQuote
|
||||||
case r == ' ' && !inQuote:
|
case r == ' ' && !inQuote:
|
||||||
if current.Len() > 0 || wasQuoted {
|
if current.Len() > 0 || wasQuoted {
|
||||||
parts = append(parts, current.String())
|
parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted})
|
||||||
current.Reset()
|
current.Reset()
|
||||||
wasQuoted = false
|
wasQuoted = false
|
||||||
}
|
}
|
||||||
@@ -485,7 +512,7 @@ func (n *NiriProvider) parseActionParts(action string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if current.Len() > 0 || wasQuoted {
|
if current.Len() > 0 || wasQuoted {
|
||||||
parts = append(parts, current.String())
|
parts = append(parts, niriActionPart{value: current.String(), quoted: wasQuoted})
|
||||||
}
|
}
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type NiriSection struct {
|
|||||||
|
|
||||||
type NiriParser struct {
|
type NiriParser struct {
|
||||||
configDir string
|
configDir string
|
||||||
|
modKey string
|
||||||
processedFiles map[string]bool
|
processedFiles map[string]bool
|
||||||
bindMap map[string]*NiriKeyBinding
|
bindMap map[string]*NiriKeyBinding
|
||||||
bindOrder []string
|
bindOrder []string
|
||||||
@@ -237,6 +238,7 @@ func isBraceAdjacentSpace(b byte) bool {
|
|||||||
func NewNiriParser(configDir string) *NiriParser {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
return &NiriParser{
|
return &NiriParser{
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
|
modKey: "Super",
|
||||||
processedFiles: make(map[string]bool),
|
processedFiles: make(map[string]bool),
|
||||||
bindMap: make(map[string]*NiriKeyBinding),
|
bindMap: make(map[string]*NiriKeyBinding),
|
||||||
bindOrder: []string{},
|
bindOrder: []string{},
|
||||||
@@ -377,6 +379,8 @@ func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection,
|
|||||||
switch name {
|
switch name {
|
||||||
case "include":
|
case "include":
|
||||||
p.handleInclude(node, section, baseDir)
|
p.handleInclude(node, section, baseDir)
|
||||||
|
case "input":
|
||||||
|
p.handleInput(node)
|
||||||
case "binds":
|
case "binds":
|
||||||
p.extractBinds(node, section, "")
|
p.extractBinds(node, section, "")
|
||||||
case "recent-windows":
|
case "recent-windows":
|
||||||
@@ -385,6 +389,19 @@ func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) handleInput(node *document.Node) {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
if child.Name.String() != "mod-key" || len(child.Arguments) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
modKey := strings.Trim(strings.TrimSpace(child.Arguments[0].String()), "\"")
|
||||||
|
if modKey != "" {
|
||||||
|
p.modKey = modKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) {
|
func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) {
|
||||||
if len(node.Arguments) == 0 {
|
if len(node.Arguments) == 0 {
|
||||||
return
|
return
|
||||||
@@ -534,6 +551,7 @@ func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
|
|||||||
|
|
||||||
type NiriParseResult struct {
|
type NiriParseResult struct {
|
||||||
Section *NiriSection
|
Section *NiriSection
|
||||||
|
ModKey string
|
||||||
DMSBindsIncluded bool
|
DMSBindsIncluded bool
|
||||||
DMSStatus *DMSBindsStatusInfo
|
DMSStatus *DMSBindsStatusInfo
|
||||||
ConflictingConfigs map[string]*NiriKeyBinding
|
ConflictingConfigs map[string]*NiriKeyBinding
|
||||||
@@ -586,6 +604,7 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
|
|||||||
}
|
}
|
||||||
return &NiriParseResult{
|
return &NiriParseResult{
|
||||||
Section: section,
|
Section: section,
|
||||||
|
ModKey: parser.modKey,
|
||||||
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
|
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
|
||||||
DMSStatus: parser.buildDMSStatus(),
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
ConflictingConfigs: parser.conflictingConfigs,
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
|
|||||||
@@ -7,6 +7,28 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNiriParseModKey(t *testing.T) {
|
||||||
|
config := `input {
|
||||||
|
mod-key "Alt"
|
||||||
|
}
|
||||||
|
binds {
|
||||||
|
Mod+T { spawn "kitty"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.ModKey != "Alt" {
|
||||||
|
t.Errorf("ModKey = %q, want %q", result.ModKey, "Alt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
|
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
|
||||||
config := `recent-windows {
|
config := `recent-windows {
|
||||||
binds {
|
binds {
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
content := `binds {
|
content := `input {
|
||||||
|
mod-key "Alt"
|
||||||
|
}
|
||||||
|
binds {
|
||||||
Mod+Q { close-window; }
|
Mod+Q { close-window; }
|
||||||
Mod+F { fullscreen-window; }
|
Mod+F { fullscreen-window; }
|
||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
@@ -45,6 +48,10 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
|
|||||||
t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri")
|
t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cheatSheet.ModKey != "Alt" {
|
||||||
|
t.Errorf("ModKey = %q, want %q", cheatSheet.ModKey, "Alt")
|
||||||
|
}
|
||||||
|
|
||||||
windowBinds := cheatSheet.Binds["Window"]
|
windowBinds := cheatSheet.Binds["Window"]
|
||||||
if len(windowBinds) < 2 {
|
if len(windowBinds) < 2 {
|
||||||
t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds))
|
t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds))
|
||||||
@@ -227,6 +234,58 @@ func TestNiriGenerateBindsContent(t *testing.T) {
|
|||||||
expected: `binds {
|
expected: `binds {
|
||||||
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spawn with equals arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+B": {
|
||||||
|
Key: "Mod+B",
|
||||||
|
Action: `spawn /opt/browser --profile-directory=Default`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+B { spawn "/opt/browser" "--profile-directory=Default"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spawn shell command with quoted equals args",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+C": {
|
||||||
|
Key: "Mod+C",
|
||||||
|
Action: `spawn sh -c "chrome --profile-directory=Default --app=x"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+C { spawn "sh" "-c" "chrome --profile-directory=Default --app=x"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spawn env assignment stays arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+E": {
|
||||||
|
Key: "Mod+E",
|
||||||
|
Action: `spawn env FOO=bar mycmd`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+E { spawn "env" "FOO=bar" "mycmd"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "niri action property remains property",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Print": {
|
||||||
|
Key: "Print",
|
||||||
|
Action: `screenshot show-pointer=false`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Print { screenshot show-pointer=false; }
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type DMSBindsStatus struct {
|
|||||||
type CheatSheet struct {
|
type CheatSheet struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
ModKey string `json:"modKey,omitempty"`
|
||||||
Binds map[string][]Keybind `json:"binds"`
|
Binds map[string][]Keybind `json:"binds"`
|
||||||
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
|
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
|
||||||
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
|
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
|
||||||
|
|||||||
@@ -118,6 +118,50 @@ type ColorsOutput struct {
|
|||||||
} `json:"colors"`
|
} `json:"colors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SchemePreview struct {
|
||||||
|
Dark string `json:"dark"`
|
||||||
|
Light string `json:"light"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var previewSchemeTypes = []string{
|
||||||
|
"scheme-tonal-spot",
|
||||||
|
"scheme-vibrant",
|
||||||
|
"scheme-content",
|
||||||
|
"scheme-expressive",
|
||||||
|
"scheme-fidelity",
|
||||||
|
"scheme-fruit-salad",
|
||||||
|
"scheme-monochrome",
|
||||||
|
"scheme-neutral",
|
||||||
|
"scheme-rainbow",
|
||||||
|
}
|
||||||
|
|
||||||
|
func PreviewSchemes(sourceColor string, contrast float64) (map[string]SchemePreview, error) {
|
||||||
|
if sourceColor == "" {
|
||||||
|
return nil, fmt.Errorf("source color is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
previews := make(map[string]SchemePreview, len(previewSchemeTypes))
|
||||||
|
for _, schemeType := range previewSchemeTypes {
|
||||||
|
output, err := runMatugenDryRun(&Options{
|
||||||
|
Kind: "hex",
|
||||||
|
Value: sourceColor,
|
||||||
|
MatugenType: schemeType,
|
||||||
|
Contrast: contrast,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("preview %s: %w", schemeType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dark := extractMatugenColor(output, "primary", "dark")
|
||||||
|
light := extractMatugenColor(output, "primary", "light")
|
||||||
|
if dark == "" || light == "" {
|
||||||
|
return nil, fmt.Errorf("preview %s: primary colors missing from matugen output", schemeType)
|
||||||
|
}
|
||||||
|
previews[schemeType] = SchemePreview{Dark: dark, Light: light}
|
||||||
|
}
|
||||||
|
return previews, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Options) ColorsOutput() string {
|
func (o *Options) ColorsOutput() string {
|
||||||
return filepath.Join(o.StateDir, "dms-colors.json")
|
return filepath.Join(o.StateDir, "dms-colors.json")
|
||||||
}
|
}
|
||||||
@@ -655,6 +699,7 @@ func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
|
|||||||
|
|
||||||
func detectMatugenVersionLocked() (matugenFlags, error) {
|
func detectMatugenVersionLocked() (matugenFlags, error) {
|
||||||
cmd := exec.Command("matugen", "--version")
|
cmd := exec.Command("matugen", "--version")
|
||||||
|
cmd.Env = utils.EnvWithUserBinPath(nil)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
|
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
|
||||||
@@ -711,6 +756,7 @@ func runMatugen(baseArgs []string) error {
|
|||||||
|
|
||||||
args := buildMatugenArgs(baseArgs, flags)
|
args := buildMatugenArgs(baseArgs, flags)
|
||||||
cmd := exec.Command("matugen", args...)
|
cmd := exec.Command("matugen", args...)
|
||||||
|
cmd.Env = utils.EnvWithUserBinPath(nil)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
runErr := cmd.Run()
|
runErr := cmd.Run()
|
||||||
@@ -728,6 +774,7 @@ func runMatugen(baseArgs []string) error {
|
|||||||
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
|
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
|
||||||
args = buildMatugenArgs(baseArgs, newFlags)
|
args = buildMatugenArgs(baseArgs, newFlags)
|
||||||
retryCmd := exec.Command("matugen", args...)
|
retryCmd := exec.Command("matugen", args...)
|
||||||
|
retryCmd.Env = utils.EnvWithUserBinPath(nil)
|
||||||
retryCmd.Stdout = os.Stdout
|
retryCmd.Stdout = os.Stdout
|
||||||
retryCmd.Stderr = os.Stderr
|
retryCmd.Stderr = os.Stderr
|
||||||
return retryCmd.Run()
|
return retryCmd.Run()
|
||||||
@@ -770,6 +817,7 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("matugen", baseArgs...)
|
cmd := exec.Command("matugen", baseArgs...)
|
||||||
|
cmd.Env = utils.EnvWithUserBinPath(nil)
|
||||||
var stderr strings.Builder
|
var stderr strings.Builder
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
@@ -868,7 +916,26 @@ func refreshGTK(mode ColorMode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var colorSchemeEchoHook func(scheme string)
|
||||||
|
|
||||||
|
func SetColorSchemeEchoHook(hook func(scheme string)) {
|
||||||
|
colorSchemeEchoHook = hook
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectColorSchemeEcho(scheme string) {
|
||||||
|
if colorSchemeEchoHook != nil {
|
||||||
|
colorSchemeEchoHook(scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The color-scheme round trip is the only mechanism that makes running GTK4
|
||||||
|
// apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps
|
||||||
|
// following the portal color-scheme (Chromium) can drop the restore signal
|
||||||
|
// mid-repaint and latch the wrong mode, so this is opt-in.
|
||||||
func refreshGTK4() {
|
func refreshGTK4() {
|
||||||
|
if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
|
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -882,11 +949,13 @@ func refreshGTK4() {
|
|||||||
toggle = "prefer-dark"
|
toggle = "prefer-dark"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectColorSchemeEcho(toggle)
|
||||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
|
||||||
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
|
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(400 * time.Millisecond)
|
||||||
|
expectColorSchemeEcho(current)
|
||||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
|
||||||
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
|
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -554,35 +554,50 @@ func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
|
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (hasUpdates bool, diffURL string, err error) {
|
||||||
pluginPath, err := m.findInstalledPath(pluginID)
|
pluginPath, err := m.findInstalledPath(pluginID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to find plugin: %w", err)
|
return false, "", fmt.Errorf("failed to find plugin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pluginPath == "" {
|
if pluginPath == "" {
|
||||||
return false, fmt.Errorf("plugin not installed: %s", pluginID)
|
return false, "", fmt.Errorf("plugin not installed: %s", pluginID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
|
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
|
||||||
return false, nil
|
return false, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
metaPath := pluginPath + ".meta"
|
metaPath := pluginPath + ".meta"
|
||||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to check metadata: %w", err)
|
return false, "", fmt.Errorf("failed to check metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hasUp bool
|
||||||
|
var localHash, remoteHash string
|
||||||
if metaExists {
|
if metaExists {
|
||||||
// Plugin is from a monorepo, check the repo directory
|
// Plugin is from a monorepo, check the repo directory
|
||||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||||
repoName := m.getRepoName(plugin.Repo)
|
repoName := m.getRepoName(plugin.Repo)
|
||||||
repoPath := filepath.Join(reposDir, repoName)
|
repoPath := filepath.Join(reposDir, repoName)
|
||||||
|
|
||||||
return m.gitClient.HasUpdates(repoPath)
|
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(repoPath)
|
||||||
|
} else {
|
||||||
|
// Plugin is a standalone repo
|
||||||
|
hasUp, localHash, remoteHash, err = m.gitClient.HasUpdates(pluginPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin is a standalone repo
|
if err != nil {
|
||||||
return m.gitClient.HasUpdates(pluginPath)
|
return false, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
diffURL = plugin.Repo
|
||||||
|
if diffURL != "" {
|
||||||
|
diffURL = strings.TrimSuffix(diffURL, ".git")
|
||||||
|
if hasUp && len(localHash) >= 7 && len(remoteHash) >= 7 {
|
||||||
|
diffURL = fmt.Sprintf("%s/compare/%s...%s", diffURL, localHash[:7], remoteHash[:7])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasUp, diffURL, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type Plugin struct {
|
|||||||
type GitClient interface {
|
type GitClient interface {
|
||||||
PlainClone(path string, url string) error
|
PlainClone(path string, url string) error
|
||||||
Pull(path string) error
|
Pull(path string) error
|
||||||
HasUpdates(path string) (bool, error)
|
HasUpdates(path string) (hasUpdates bool, localHash string, remoteHash string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type realGitClient struct{}
|
type realGitClient struct{}
|
||||||
@@ -65,10 +65,10 @@ func (g *realGitClient) Pull(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *realGitClient) HasUpdates(path string) (bool, error) {
|
func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
|
||||||
repo, err := git.PlainOpen(path)
|
repo, err := git.PlainOpen(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch remote changes
|
// Fetch remote changes
|
||||||
@@ -76,24 +76,24 @@ func (g *realGitClient) HasUpdates(path string) (bool, error) {
|
|||||||
if err != nil && err.Error() != "already up-to-date" {
|
if err != nil && err.Error() != "already up-to-date" {
|
||||||
// If fetch fails, we can't determine if there are updates
|
// If fetch fails, we can't determine if there are updates
|
||||||
// Return false and the error
|
// Return false and the error
|
||||||
return false, err
|
return false, "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the HEAD reference
|
// Get the HEAD reference
|
||||||
head, err := repo.Head()
|
head, err := repo.Head()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
|
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
|
||||||
remote, err := repo.Remote("origin")
|
remote, err := repo.Remote("origin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
refs, err := remote.List(&git.ListOptions{})
|
refs, err := remote.List(&git.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the default branch remote ref
|
// Find the default branch remote ref
|
||||||
@@ -108,13 +108,14 @@ func (g *realGitClient) HasUpdates(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localHash := head.Hash().String()
|
||||||
// If we couldn't find a remote HEAD, assume no updates
|
// If we couldn't find a remote HEAD, assume no updates
|
||||||
if remoteHead == "" {
|
if remoteHead == "" {
|
||||||
return false, nil
|
return false, localHash, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare local HEAD with remote HEAD
|
// Compare local HEAD with remote HEAD
|
||||||
return head.Hash().String() != remoteHead, nil
|
return localHash != remoteHead, localHash, remoteHead, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
type mockGitClient struct {
|
type mockGitClient struct {
|
||||||
cloneFunc func(path string, url string) error
|
cloneFunc func(path string, url string) error
|
||||||
pullFunc func(path string) error
|
pullFunc func(path string) error
|
||||||
hasUpdatesFunc func(path string) (bool, error)
|
hasUpdatesFunc func(path string) (bool, string, string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockGitClient) PlainClone(path string, url string) error {
|
func (m *mockGitClient) PlainClone(path string, url string) error {
|
||||||
@@ -30,11 +30,11 @@ func (m *mockGitClient) Pull(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockGitClient) HasUpdates(path string) (bool, error) {
|
func (m *mockGitClient) HasUpdates(path string) (bool, string, string, error) {
|
||||||
if m.hasUpdatesFunc != nil {
|
if m.hasUpdatesFunc != nil {
|
||||||
return m.hasUpdatesFunc(path)
|
return m.hasUpdatesFunc(path)
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, "", "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewRegistry(t *testing.T) {
|
func TestNewRegistry(t *testing.T) {
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : internal/proto/xml/virtual-keyboard-unstable-v1.xml
|
||||||
|
//
|
||||||
|
// virtual_keyboard_unstable_v1 Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2008-2011 Kristian Høgsberg
|
||||||
|
// Copyright © 2010-2013 Intel Corporation
|
||||||
|
// Copyright © 2012-2013 Collabora, Ltd.
|
||||||
|
// Copyright © 2018 Purism SPC
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
// copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice (including the next
|
||||||
|
// paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
// Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package virtual_keyboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ZwpVirtualKeyboardV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwpVirtualKeyboardV1InterfaceName = "zwp_virtual_keyboard_v1"
|
||||||
|
|
||||||
|
// ZwpVirtualKeyboardV1 : virtual keyboard
|
||||||
|
//
|
||||||
|
// The virtual keyboard provides an application with requests which emulate
|
||||||
|
// the behaviour of a physical keyboard.
|
||||||
|
//
|
||||||
|
// This interface can be used by clients on its own to provide raw input
|
||||||
|
// events, or it can accompany the input method protocol.
|
||||||
|
type ZwpVirtualKeyboardV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwpVirtualKeyboardV1 : virtual keyboard
|
||||||
|
//
|
||||||
|
// The virtual keyboard provides an application with requests which emulate
|
||||||
|
// the behaviour of a physical keyboard.
|
||||||
|
//
|
||||||
|
// This interface can be used by clients on its own to provide raw input
|
||||||
|
// events, or it can accompany the input method protocol.
|
||||||
|
func NewZwpVirtualKeyboardV1(ctx *client.Context) *ZwpVirtualKeyboardV1 {
|
||||||
|
zwpVirtualKeyboardV1 := &ZwpVirtualKeyboardV1{}
|
||||||
|
ctx.Register(zwpVirtualKeyboardV1)
|
||||||
|
return zwpVirtualKeyboardV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keymap : keyboard mapping
|
||||||
|
//
|
||||||
|
// Provide a file descriptor to the compositor which can be
|
||||||
|
// memory-mapped to provide a keyboard mapping description.
|
||||||
|
//
|
||||||
|
// Format carries a value from the keymap_format enumeration.
|
||||||
|
//
|
||||||
|
// format: keymap format
|
||||||
|
// fd: keymap file descriptor
|
||||||
|
// size: keymap size, in bytes
|
||||||
|
func (i *ZwpVirtualKeyboardV1) Keymap(format uint32, fd int, size uint32) error {
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(format))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(size))
|
||||||
|
l += 4
|
||||||
|
oob := unix.UnixRights(int(fd))
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], oob)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key : key event
|
||||||
|
//
|
||||||
|
// A key was pressed or released.
|
||||||
|
// The time argument is a timestamp with millisecond granularity, with an
|
||||||
|
// undefined base. All requests regarding a single object must share the
|
||||||
|
// same clock.
|
||||||
|
//
|
||||||
|
// Keymap must be set before issuing this request.
|
||||||
|
//
|
||||||
|
// State carries a value from the key_state enumeration.
|
||||||
|
//
|
||||||
|
// time: timestamp with millisecond granularity
|
||||||
|
// key: key that produced the event
|
||||||
|
// state: physical state of the key
|
||||||
|
func (i *ZwpVirtualKeyboardV1) Key(time, key, state uint32) error {
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(time))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(key))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(state))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifiers : modifier and group state
|
||||||
|
//
|
||||||
|
// Notifies the compositor that the modifier and/or group state has
|
||||||
|
// changed, and it should update state.
|
||||||
|
//
|
||||||
|
// The client should use wl_keyboard.modifiers event to synchronize its
|
||||||
|
// internal state with seat state.
|
||||||
|
//
|
||||||
|
// Keymap must be set before issuing this request.
|
||||||
|
//
|
||||||
|
// modsDepressed: depressed modifiers
|
||||||
|
// modsLatched: latched modifiers
|
||||||
|
// modsLocked: locked modifiers
|
||||||
|
// group: keyboard layout
|
||||||
|
func (i *ZwpVirtualKeyboardV1) Modifiers(modsDepressed, modsLatched, modsLocked, group uint32) error {
|
||||||
|
const opcode = 2
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(modsDepressed))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(modsLatched))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(modsLocked))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(group))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the virtual keyboard keyboard object
|
||||||
|
func (i *ZwpVirtualKeyboardV1) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 3
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwpVirtualKeyboardV1Error uint32
|
||||||
|
|
||||||
|
// ZwpVirtualKeyboardV1Error :
|
||||||
|
const (
|
||||||
|
// ZwpVirtualKeyboardV1ErrorNoKeymap : No keymap was set
|
||||||
|
ZwpVirtualKeyboardV1ErrorNoKeymap ZwpVirtualKeyboardV1Error = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwpVirtualKeyboardV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpVirtualKeyboardV1ErrorNoKeymap:
|
||||||
|
return "no_keymap"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpVirtualKeyboardV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpVirtualKeyboardV1ErrorNoKeymap:
|
||||||
|
return "0"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpVirtualKeyboardV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwpVirtualKeyboardManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwpVirtualKeyboardManagerV1InterfaceName = "zwp_virtual_keyboard_manager_v1"
|
||||||
|
|
||||||
|
// ZwpVirtualKeyboardManagerV1 : virtual keyboard manager
|
||||||
|
//
|
||||||
|
// A virtual keyboard manager allows an application to provide keyboard
|
||||||
|
// input events as if they came from a physical keyboard.
|
||||||
|
type ZwpVirtualKeyboardManagerV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwpVirtualKeyboardManagerV1 : virtual keyboard manager
|
||||||
|
//
|
||||||
|
// A virtual keyboard manager allows an application to provide keyboard
|
||||||
|
// input events as if they came from a physical keyboard.
|
||||||
|
func NewZwpVirtualKeyboardManagerV1(ctx *client.Context) *ZwpVirtualKeyboardManagerV1 {
|
||||||
|
zwpVirtualKeyboardManagerV1 := &ZwpVirtualKeyboardManagerV1{}
|
||||||
|
ctx.Register(zwpVirtualKeyboardManagerV1)
|
||||||
|
return zwpVirtualKeyboardManagerV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateVirtualKeyboard : Create a new virtual keyboard
|
||||||
|
//
|
||||||
|
// Creates a new virtual keyboard associated to a seat.
|
||||||
|
//
|
||||||
|
// If the compositor enables a keyboard to perform arbitrary actions, it
|
||||||
|
// should present an error when an untrusted client requests a new
|
||||||
|
// keyboard.
|
||||||
|
func (i *ZwpVirtualKeyboardManagerV1) CreateVirtualKeyboard(seat *client.Seat) (*ZwpVirtualKeyboardV1, error) {
|
||||||
|
id := NewZwpVirtualKeyboardV1(i.Context())
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], seat.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ZwpVirtualKeyboardManagerV1) Destroy() error {
|
||||||
|
i.Context().Unregister(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwpVirtualKeyboardManagerV1Error uint32
|
||||||
|
|
||||||
|
// ZwpVirtualKeyboardManagerV1Error :
|
||||||
|
const (
|
||||||
|
// ZwpVirtualKeyboardManagerV1ErrorUnauthorized : client not authorized to use the interface
|
||||||
|
ZwpVirtualKeyboardManagerV1ErrorUnauthorized ZwpVirtualKeyboardManagerV1Error = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwpVirtualKeyboardManagerV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpVirtualKeyboardManagerV1ErrorUnauthorized:
|
||||||
|
return "unauthorized"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpVirtualKeyboardManagerV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpVirtualKeyboardManagerV1ErrorUnauthorized:
|
||||||
|
return "0"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpVirtualKeyboardManagerV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="virtual_keyboard_unstable_v1">
|
||||||
|
<copyright>
|
||||||
|
Copyright © 2008-2011 Kristian Høgsberg
|
||||||
|
Copyright © 2010-2013 Intel Corporation
|
||||||
|
Copyright © 2012-2013 Collabora, Ltd.
|
||||||
|
Copyright © 2018 Purism SPC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<interface name="zwp_virtual_keyboard_v1" version="1">
|
||||||
|
<description summary="virtual keyboard">
|
||||||
|
The virtual keyboard provides an application with requests which emulate
|
||||||
|
the behaviour of a physical keyboard.
|
||||||
|
|
||||||
|
This interface can be used by clients on its own to provide raw input
|
||||||
|
events, or it can accompany the input method protocol.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="keymap">
|
||||||
|
<description summary="keyboard mapping">
|
||||||
|
Provide a file descriptor to the compositor which can be
|
||||||
|
memory-mapped to provide a keyboard mapping description.
|
||||||
|
|
||||||
|
Format carries a value from the keymap_format enumeration.
|
||||||
|
</description>
|
||||||
|
<arg name="format" type="uint" summary="keymap format"/>
|
||||||
|
<arg name="fd" type="fd" summary="keymap file descriptor"/>
|
||||||
|
<arg name="size" type="uint" summary="keymap size, in bytes"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="no_keymap" value="0" summary="No keymap was set"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="key">
|
||||||
|
<description summary="key event">
|
||||||
|
A key was pressed or released.
|
||||||
|
The time argument is a timestamp with millisecond granularity, with an
|
||||||
|
undefined base. All requests regarding a single object must share the
|
||||||
|
same clock.
|
||||||
|
|
||||||
|
Keymap must be set before issuing this request.
|
||||||
|
|
||||||
|
State carries a value from the key_state enumeration.
|
||||||
|
</description>
|
||||||
|
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
|
||||||
|
<arg name="key" type="uint" summary="key that produced the event"/>
|
||||||
|
<arg name="state" type="uint" summary="physical state of the key"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="modifiers">
|
||||||
|
<description summary="modifier and group state">
|
||||||
|
Notifies the compositor that the modifier and/or group state has
|
||||||
|
changed, and it should update state.
|
||||||
|
|
||||||
|
The client should use wl_keyboard.modifiers event to synchronize its
|
||||||
|
internal state with seat state.
|
||||||
|
|
||||||
|
Keymap must be set before issuing this request.
|
||||||
|
</description>
|
||||||
|
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
|
||||||
|
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
|
||||||
|
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
|
||||||
|
<arg name="group" type="uint" summary="keyboard layout"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor" since="1">
|
||||||
|
<description summary="destroy the virtual keyboard keyboard object"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="zwp_virtual_keyboard_manager_v1" version="1">
|
||||||
|
<description summary="virtual keyboard manager">
|
||||||
|
A virtual keyboard manager allows an application to provide keyboard
|
||||||
|
input events as if they came from a physical keyboard.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="create_virtual_keyboard">
|
||||||
|
<description summary="Create a new virtual keyboard">
|
||||||
|
Creates a new virtual keyboard associated to a seat.
|
||||||
|
|
||||||
|
If the compositor enables a keyboard to perform arbitrary actions, it
|
||||||
|
should present an error when an untrusted client requests a new
|
||||||
|
keyboard.
|
||||||
|
</description>
|
||||||
|
<arg name="seat" type="object" interface="wl_seat"/>
|
||||||
|
<arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
@@ -68,6 +68,12 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
|||||||
activeBuses[i] = true
|
activeBuses[i] = true
|
||||||
id := fmt.Sprintf("ddc:i2c-%d", i)
|
id := fmt.Sprintf("ddc:i2c-%d", i)
|
||||||
|
|
||||||
|
// Don't re-probe identified monitors: DDC traffic during a wake
|
||||||
|
// sequence can disturb some monitors' own brightness handling.
|
||||||
|
if _, ok := b.devices.Load(id); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
dev, err := b.probeDDCDevice(i)
|
dev, err := b.probeDDCDevice(i)
|
||||||
if err != nil || dev == nil {
|
if err != nil || dev == nil {
|
||||||
continue
|
continue
|
||||||
@@ -107,7 +113,8 @@ func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) {
|
|||||||
dummy := make([]byte, 32)
|
dummy := make([]byte, 32)
|
||||||
syscall.Read(fd, dummy) //nolint:errcheck
|
syscall.Read(fd, dummy) //nolint:errcheck
|
||||||
|
|
||||||
writebuf := []byte{0x00}
|
writebuf := []byte{DDC_SOURCE_ADDR, 0x80}
|
||||||
|
writebuf = append(writebuf, ddcciChecksum(writebuf))
|
||||||
n, err := syscall.Write(fd, writebuf)
|
n, err := syscall.Write(fd, writebuf)
|
||||||
if err == nil && n == len(writebuf) {
|
if err == nil && n == len(writebuf) {
|
||||||
name := b.getDDCName(bus)
|
name := b.getDDCName(bus)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||||
)
|
)
|
||||||
@@ -28,6 +29,10 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
handleCopyEntry(conn, req, m)
|
handleCopyEntry(conn, req, m)
|
||||||
case "clipboard.paste":
|
case "clipboard.paste":
|
||||||
handlePaste(conn, req, m)
|
handlePaste(conn, req, m)
|
||||||
|
case "clipboard.sendPaste":
|
||||||
|
handleSendPaste(conn, req)
|
||||||
|
case "clipboard.pasteSupported":
|
||||||
|
models.Respond(conn, req.ID, map[string]bool{"supported": m.pasteSupported})
|
||||||
case "clipboard.subscribe":
|
case "clipboard.subscribe":
|
||||||
handleSubscribe(conn, req, m)
|
handleSubscribe(conn, req, m)
|
||||||
case "clipboard.search":
|
case "clipboard.search":
|
||||||
@@ -176,6 +181,17 @@ func handlePaste(conn net.Conn, req models.Request, m *Manager) {
|
|||||||
models.Respond(conn, req.ID, map[string]string{"text": text})
|
models.Respond(conn, req.ID, map[string]string{"text": text})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSendPaste(conn net.Conn, req models.Request) {
|
||||||
|
shift, _ := models.Get[bool](req, "shift")
|
||||||
|
|
||||||
|
if err := clipboardstore.SendPasteKeystroke(shift); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "paste sent"})
|
||||||
|
}
|
||||||
|
|
||||||
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
|
func handleSubscribe(conn net.Conn, req models.Request, m *Manager) {
|
||||||
clientID := fmt.Sprintf("clipboard-%d", req.ID)
|
clientID := fmt.Sprintf("clipboard-%d", req.ID)
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
clipboardstore "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
@@ -164,6 +165,8 @@ func (m *Manager) setupRegistry() error {
|
|||||||
m.seat = seat
|
m.seat = seat
|
||||||
m.seatName = e.Name
|
m.seatName = e.Name
|
||||||
log.Info("Bound wl_seat")
|
log.Info("Bound wl_seat")
|
||||||
|
case virtual_keyboard.ZwpVirtualKeyboardManagerV1InterfaceName:
|
||||||
|
m.pasteSupported = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1054,6 +1057,13 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
dataCopy := make([]byte, len(data))
|
dataCopy := make([]byte, len(data))
|
||||||
copy(dataCopy, data)
|
copy(dataCopy, data)
|
||||||
|
|
||||||
|
m.takeSelection(clipboardstore.ExpandOffers(dataCopy, mimeType))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// takeSelection makes the daemon the selection owner, serving the given
|
||||||
|
// offers until another client claims the clipboard.
|
||||||
|
func (m *Manager) takeSelection(offers []clipboardstore.Offer) {
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
if m.dataControlMgr == nil || m.dataDevice == nil {
|
if m.dataControlMgr == nil || m.dataDevice == nil {
|
||||||
log.Error("Data control manager or device not initialized")
|
log.Error("Data control manager or device not initialized")
|
||||||
@@ -1068,10 +1078,14 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := source.Offer(mimeType); err != nil {
|
offerData := make(map[string][]byte, len(offers))
|
||||||
log.Errorf("Failed to offer mime type: %v", err)
|
for _, offer := range offers {
|
||||||
|
if err := source.Offer(offer.MimeType); err != nil {
|
||||||
|
log.Errorf("Failed to offer %s: %v", offer.MimeType, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
offerData[offer.MimeType] = offer.Data
|
||||||
|
}
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
fd := e.Fd
|
fd := e.Fd
|
||||||
@@ -1080,7 +1094,11 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
file := os.NewFile(uintptr(fd), "clipboard-pipe")
|
file := os.NewFile(uintptr(fd), "clipboard-pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if _, err := file.Write(dataCopy); err != nil {
|
data, ok := offerData[e.MimeType]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := file.Write(data); err != nil {
|
||||||
log.Errorf("Failed to write clipboard data: %v", err)
|
log.Errorf("Failed to write clipboard data: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1093,9 +1111,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
|
|
||||||
m.releaseCurrentSource()
|
m.releaseCurrentSource()
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
m.sourceMutex.Lock()
|
|
||||||
m.sourceMimeTypes = []string{mimeType}
|
|
||||||
m.sourceMutex.Unlock()
|
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
m.ownerLock.Lock()
|
||||||
m.isOwner = true
|
m.isOwner = true
|
||||||
@@ -1106,8 +1121,6 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
log.Errorf("Failed to set selection: %v", err)
|
log.Errorf("Failed to set selection: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) CopyText(text string) error {
|
func (m *Manager) CopyText(text string) error {
|
||||||
@@ -1779,74 +1792,16 @@ func (m *Manager) CopyFile(filePath string) error {
|
|||||||
m.updateState()
|
m.updateState()
|
||||||
m.notifySubscribers()
|
m.notifySubscribers()
|
||||||
|
|
||||||
_, imgMime, imgErr := image.DecodeConfig(bytes.NewReader(fileData))
|
offers := []clipboardstore.Offer{
|
||||||
|
{MimeType: "x-special/gnome-copied-files", Data: []byte("copy\n" + fileURI)},
|
||||||
m.post(func() {
|
{MimeType: "text/uri-list", Data: []byte(fileURI + "\r\n")},
|
||||||
if m.dataControlMgr == nil || m.dataDevice == nil {
|
{MimeType: "text/plain", Data: []byte(filePath)},
|
||||||
log.Error("Data control manager or device not initialized")
|
}
|
||||||
return
|
if _, imgMime, err := image.DecodeConfig(bytes.NewReader(fileData)); err == nil {
|
||||||
|
offers = append(offers, clipboardstore.Offer{MimeType: "image/" + imgMime, Data: fileData})
|
||||||
}
|
}
|
||||||
|
|
||||||
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
|
m.takeSelection(offers)
|
||||||
source, err := dataMgr.CreateDataSource()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create data source: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type offer struct {
|
|
||||||
mime string
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
offers := []offer{
|
|
||||||
{"x-special/gnome-copied-files", []byte("copy\n" + fileURI)},
|
|
||||||
{"text/uri-list", []byte(fileURI + "\r\n")},
|
|
||||||
{"text/plain", []byte(filePath)},
|
|
||||||
}
|
|
||||||
|
|
||||||
if imgErr == nil {
|
|
||||||
imgMimeType := "image/" + imgMime
|
|
||||||
offers = append(offers, offer{imgMimeType, fileData})
|
|
||||||
}
|
|
||||||
|
|
||||||
offerData := make(map[string][]byte)
|
|
||||||
for _, o := range offers {
|
|
||||||
if err := source.Offer(o.mime); err != nil {
|
|
||||||
log.Errorf("Failed to offer %s: %v", o.mime, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerData[o.mime] = o.data
|
|
||||||
}
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
|
||||||
fd := e.Fd
|
|
||||||
defer syscall.Close(fd)
|
|
||||||
file := os.NewFile(uintptr(fd), "clipboard-pipe")
|
|
||||||
defer file.Close()
|
|
||||||
if data, ok := offerData[e.MimeType]; ok {
|
|
||||||
file.Write(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = false
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
})
|
|
||||||
|
|
||||||
m.releaseCurrentSource()
|
|
||||||
m.currentSource = source
|
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
|
||||||
m.isOwner = true
|
|
||||||
m.ownerLock.Unlock()
|
|
||||||
|
|
||||||
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
|
||||||
if err := device.SetSelection(source); err != nil {
|
|
||||||
log.Errorf("Failed to set selection: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -531,47 +531,6 @@ func TestManager_ConcurrentOfferAccess(t *testing.T) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_ConcurrentPersistAccess(t *testing.T) {
|
|
||||||
m := &Manager{
|
|
||||||
persistData: make(map[string][]byte),
|
|
||||||
persistMimeTypes: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
const goroutines = 20
|
|
||||||
const iterations = 50
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.persistMutex.RLock()
|
|
||||||
_ = m.persistData
|
|
||||||
_ = m.persistMimeTypes
|
|
||||||
m.persistMutex.RUnlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < goroutines/2; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(id int) {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < iterations; j++ {
|
|
||||||
m.persistMutex.Lock()
|
|
||||||
m.persistMimeTypes = []string{"text/plain", "text/html"}
|
|
||||||
m.persistData = map[string][]byte{
|
|
||||||
"text/plain": []byte("test"),
|
|
||||||
}
|
|
||||||
m.persistMutex.Unlock()
|
|
||||||
}
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_ConcurrentOwnerAccess(t *testing.T) {
|
func TestManager_ConcurrentOwnerAccess(t *testing.T) {
|
||||||
m := &Manager{}
|
m := &Manager{}
|
||||||
|
|
||||||
|
|||||||
@@ -132,15 +132,9 @@ type Manager struct {
|
|||||||
offerMutex sync.RWMutex
|
offerMutex sync.RWMutex
|
||||||
offerRegistry map[uint32]any
|
offerRegistry map[uint32]any
|
||||||
|
|
||||||
sourceMimeTypes []string
|
|
||||||
sourceMutex sync.RWMutex
|
|
||||||
|
|
||||||
persistData map[string][]byte
|
|
||||||
persistMimeTypes []string
|
|
||||||
persistMutex sync.RWMutex
|
|
||||||
|
|
||||||
isOwner bool
|
isOwner bool
|
||||||
ownerLock sync.Mutex
|
ownerLock sync.Mutex
|
||||||
|
pasteSupported bool
|
||||||
|
|
||||||
initialized bool
|
initialized bool
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
@@ -100,6 +101,41 @@ func (m *Manager) initializeSettings() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpectColorSchemeEcho registers a self-write so the watcher swallows its SettingChanged echo instead of forwarding it as an external change.
|
||||||
|
func (m *Manager) ExpectColorSchemeEcho(scheme string) {
|
||||||
|
var value uint32
|
||||||
|
switch scheme {
|
||||||
|
case "prefer-dark":
|
||||||
|
value = 1
|
||||||
|
case "prefer-light":
|
||||||
|
value = 2
|
||||||
|
}
|
||||||
|
m.selfEchoMu.Lock()
|
||||||
|
m.selfEchoes = append(m.selfEchoes, colorSchemeEcho{value: value, expires: time.Now().Add(10 * time.Second)})
|
||||||
|
m.selfEchoMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) consumeSelfEcho(value uint32) bool {
|
||||||
|
m.selfEchoMu.Lock()
|
||||||
|
defer m.selfEchoMu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
kept := m.selfEchoes[:0]
|
||||||
|
consumed := false
|
||||||
|
for _, echo := range m.selfEchoes {
|
||||||
|
if now.After(echo.expires) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !consumed && echo.value == value {
|
||||||
|
consumed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, echo)
|
||||||
|
}
|
||||||
|
m.selfEchoes = kept
|
||||||
|
return consumed
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) watchSettingsChanges() {
|
func (m *Manager) watchSettingsChanges() {
|
||||||
conn, err := dbus.ConnectSessionBus()
|
conn, err := dbus.ConnectSessionBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -142,13 +178,15 @@ func (m *Manager) watchSettingsChanges() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selfInflicted := m.consumeSelfEcho(colorScheme)
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
changed := m.state.Settings.ColorScheme != colorScheme || !m.state.Settings.Available
|
changed := m.state.Settings.ColorScheme != colorScheme || !m.state.Settings.Available
|
||||||
m.state.Settings.ColorScheme = colorScheme
|
m.state.Settings.ColorScheme = colorScheme
|
||||||
m.state.Settings.Available = true
|
m.state.Settings.Available = true
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
if changed {
|
if changed && !selfInflicted {
|
||||||
m.NotifySubscribers()
|
m.NotifySubscribers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,3 +141,45 @@ func TestManager_SettingsState_Modification(t *testing.T) {
|
|||||||
original := manager.GetState()
|
original := manager.GetState()
|
||||||
assert.Equal(t, uint32(0), original.Settings.ColorScheme)
|
assert.Equal(t, uint32(0), original.Settings.ColorScheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestManager_SelfEcho_ConsumesRegisteredWrites(t *testing.T) {
|
||||||
|
manager := &Manager{state: &FreedeskState{}}
|
||||||
|
|
||||||
|
manager.ExpectColorSchemeEcho("prefer-dark")
|
||||||
|
manager.ExpectColorSchemeEcho("default")
|
||||||
|
|
||||||
|
assert.True(t, manager.consumeSelfEcho(1))
|
||||||
|
assert.True(t, manager.consumeSelfEcho(0))
|
||||||
|
assert.False(t, manager.consumeSelfEcho(1))
|
||||||
|
assert.False(t, manager.consumeSelfEcho(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_SelfEcho_ExternalChangePassesThrough(t *testing.T) {
|
||||||
|
manager := &Manager{state: &FreedeskState{}}
|
||||||
|
|
||||||
|
manager.ExpectColorSchemeEcho("prefer-dark")
|
||||||
|
|
||||||
|
assert.False(t, manager.consumeSelfEcho(2))
|
||||||
|
assert.True(t, manager.consumeSelfEcho(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_SelfEcho_ConsumesOnePerRegistration(t *testing.T) {
|
||||||
|
manager := &Manager{state: &FreedeskState{}}
|
||||||
|
|
||||||
|
manager.ExpectColorSchemeEcho("prefer-dark")
|
||||||
|
manager.ExpectColorSchemeEcho("prefer-dark")
|
||||||
|
|
||||||
|
assert.True(t, manager.consumeSelfEcho(1))
|
||||||
|
assert.True(t, manager.consumeSelfEcho(1))
|
||||||
|
assert.False(t, manager.consumeSelfEcho(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_SelfEcho_SchemeMapping(t *testing.T) {
|
||||||
|
manager := &Manager{state: &FreedeskState{}}
|
||||||
|
|
||||||
|
manager.ExpectColorSchemeEcho("prefer-light")
|
||||||
|
assert.True(t, manager.consumeSelfEcho(2))
|
||||||
|
|
||||||
|
manager.ExpectColorSchemeEcho("default")
|
||||||
|
assert.True(t, manager.consumeSelfEcho(0))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ package freedesktop
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type colorSchemeEcho struct {
|
||||||
|
value uint32
|
||||||
|
expires time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type AccountsState struct {
|
type AccountsState struct {
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
UserPath string `json:"userPath"`
|
UserPath string `json:"userPath"`
|
||||||
@@ -63,4 +69,6 @@ type Manager struct {
|
|||||||
screensaverCookieCounter uint32
|
screensaverCookieCounter uint32
|
||||||
screensaverFreedesktopClaimed bool
|
screensaverFreedesktopClaimed bool
|
||||||
screensaverGnomeClaimed bool
|
screensaverGnomeClaimed bool
|
||||||
|
selfEchoMu sync.Mutex
|
||||||
|
selfEchoes []colorSchemeEcho
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,19 @@ func (m *Manager) Activate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetLockedHint(locked bool) error {
|
||||||
|
err := m.sessionObj.Call(dbusSessionInterface+".SetLockedHint", 0, locked).Err
|
||||||
|
if err != nil {
|
||||||
|
if refreshErr := m.refreshSessionBinding(); refreshErr == nil {
|
||||||
|
err = m.sessionObj.Call(dbusSessionInterface+".SetLockedHint", 0, locked).Err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set locked hint: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) SetIdleHint(idle bool) error {
|
func (m *Manager) SetIdleHint(idle bool) error {
|
||||||
err := m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err
|
err := m.sessionObj.Call(dbusSessionInterface+".SetIdleHint", 0, idle).Err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
handleActivate(conn, req, manager)
|
handleActivate(conn, req, manager)
|
||||||
case "loginctl.setIdleHint":
|
case "loginctl.setIdleHint":
|
||||||
handleSetIdleHint(conn, req, manager)
|
handleSetIdleHint(conn, req, manager)
|
||||||
|
case "loginctl.setLockedHint":
|
||||||
|
handleSetLockedHint(conn, req, manager)
|
||||||
case "loginctl.setLockBeforeSuspend":
|
case "loginctl.setLockBeforeSuspend":
|
||||||
handleSetLockBeforeSuspend(conn, req, manager)
|
handleSetLockBeforeSuspend(conn, req, manager)
|
||||||
case "loginctl.setSleepInhibitorEnabled":
|
case "loginctl.setSleepInhibitorEnabled":
|
||||||
@@ -78,6 +80,20 @@ func handleSetIdleHint(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "idle hint set"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSetLockedHint(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
locked, err := params.Bool(req.Params, "locked")
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.SetLockedHint(locked); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "locked hint set"})
|
||||||
|
}
|
||||||
|
|
||||||
func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) {
|
func handleSetLockBeforeSuspend(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
enabled, err := params.Bool(req.Params, "enabled")
|
enabled, err := params.Bool(req.Params, "enabled")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -114,6 +114,14 @@ func (b *IWDBackend) discoverDevices() error {
|
|||||||
return fmt.Errorf("failed to get managed objects: %w", err)
|
return fmt.Errorf("failed to get managed objects: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return b.applyManagedObjects(objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *IWDBackend) applyManagedObjects(objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant) error {
|
||||||
|
b.stationPath = ""
|
||||||
|
b.devicePath = ""
|
||||||
|
b.adapterPath = ""
|
||||||
|
|
||||||
for path, interfaces := range objects {
|
for path, interfaces := range objects {
|
||||||
if _, hasStation := interfaces[iwdStationInterface]; hasStation {
|
if _, hasStation := interfaces[iwdStationInterface]; hasStation {
|
||||||
b.stationPath = path
|
b.stationPath = path
|
||||||
@@ -129,6 +137,13 @@ func (b *IWDBackend) discoverDevices() error {
|
|||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if poweredVar, ok := devProps["Powered"]; ok {
|
||||||
|
if powered, ok := poweredVar.Value().(bool); ok {
|
||||||
|
b.stateMutex.Lock()
|
||||||
|
b.state.WiFiEnabled = powered
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter {
|
if _, hasAdapter := interfaces[iwdAdapterInterface]; hasAdapter {
|
||||||
@@ -136,9 +151,18 @@ func (b *IWDBackend) discoverDevices() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.stationPath == "" || b.devicePath == "" {
|
if b.devicePath == "" {
|
||||||
return fmt.Errorf("no WiFi device found")
|
return fmt.Errorf("no WiFi device found")
|
||||||
}
|
}
|
||||||
|
if b.stationPath == "" {
|
||||||
|
b.stateMutex.Lock()
|
||||||
|
b.state.WiFiEnabled = false
|
||||||
|
b.state.WiFiConnected = false
|
||||||
|
b.state.NetworkStatus = StatusDisconnected
|
||||||
|
b.state.WiFiNetworks = nil
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
log.Infof("iwd device %s has no station interface; treating WiFi as disabled", b.devicePath)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,12 +36,7 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.stationPath != "" {
|
if b.stationPath != "" {
|
||||||
err := b.conn.AddMatchSignal(
|
if err := b.addStationSignalMatch(b.stationPath); err != nil {
|
||||||
dbus.WithMatchObjectPath(b.stationPath),
|
|
||||||
dbus.WithMatchInterface(dbusPropertiesInterface),
|
|
||||||
dbus.WithMatchMember("PropertiesChanged"),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add station signal match: %w", err)
|
return fmt.Errorf("failed to add station signal match: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +77,48 @@ func (b *IWDBackend) refreshWiFiNetworkState() bool {
|
|||||||
return b.updateSavedWiFiNetworks() == nil
|
return b.updateSavedWiFiNetworks() == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *IWDBackend) addStationSignalMatch(path dbus.ObjectPath) error {
|
||||||
|
return b.conn.AddMatchSignal(
|
||||||
|
dbus.WithMatchObjectPath(path),
|
||||||
|
dbus.WithMatchInterface(dbusPropertiesInterface),
|
||||||
|
dbus.WithMatchMember("PropertiesChanged"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *IWDBackend) handleStationAdded(path dbus.ObjectPath) bool {
|
||||||
|
if path == "" || path == b.stationPath {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
b.stationPath = path
|
||||||
|
if err := b.addStationSignalMatch(path); err != nil {
|
||||||
|
log.Warnf("Failed to add iwd station signal match for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.updateState(); err != nil {
|
||||||
|
log.Warnf("Failed to update iwd state after station appeared: %v", err)
|
||||||
|
}
|
||||||
|
return b.refreshWiFiNetworkState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *IWDBackend) handleStationRemoved(path dbus.ObjectPath) bool {
|
||||||
|
if path == "" || path != b.stationPath {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
b.stationPath = ""
|
||||||
|
b.stateMutex.Lock()
|
||||||
|
b.state.WiFiEnabled = false
|
||||||
|
b.state.WiFiConnected = false
|
||||||
|
b.state.WiFiSSID = ""
|
||||||
|
b.state.WiFiSignal = 0
|
||||||
|
b.state.NetworkStatus = StatusDisconnected
|
||||||
|
b.state.WiFiNetworks = nil
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
|
func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
|
||||||
defer b.sigWG.Done()
|
defer b.sigWG.Done()
|
||||||
|
|
||||||
@@ -98,7 +136,13 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
|
|||||||
|
|
||||||
if sig.Name == dbusObjectManager+".InterfacesAdded" {
|
if sig.Name == dbusObjectManager+".InterfacesAdded" {
|
||||||
if len(sig.Body) >= 2 {
|
if len(sig.Body) >= 2 {
|
||||||
|
path, _ := sig.Body[0].(dbus.ObjectPath)
|
||||||
if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok {
|
if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok {
|
||||||
|
if _, ok := interfaces[iwdStationInterface]; ok {
|
||||||
|
if b.handleStationAdded(path) && b.onStateChange != nil {
|
||||||
|
b.onStateChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
if _, ok := interfaces[iwdKnownNetworkInterface]; ok {
|
if _, ok := interfaces[iwdKnownNetworkInterface]; ok {
|
||||||
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
|
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
|
||||||
b.onStateChange()
|
b.onStateChange()
|
||||||
@@ -111,8 +155,15 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
|
|||||||
|
|
||||||
if sig.Name == dbusObjectManager+".InterfacesRemoved" {
|
if sig.Name == dbusObjectManager+".InterfacesRemoved" {
|
||||||
if len(sig.Body) >= 2 {
|
if len(sig.Body) >= 2 {
|
||||||
|
path, _ := sig.Body[0].(dbus.ObjectPath)
|
||||||
if interfaces, ok := sig.Body[1].([]string); ok {
|
if interfaces, ok := sig.Body[1].([]string); ok {
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
|
if iface == iwdStationInterface {
|
||||||
|
if b.handleStationRemoved(path) && b.onStateChange != nil {
|
||||||
|
b.onStateChange()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
if iface == iwdKnownNetworkInterface {
|
if iface == iwdKnownNetworkInterface {
|
||||||
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
|
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
|
||||||
b.onStateChange()
|
b.onStateChange()
|
||||||
|
|||||||
@@ -169,6 +169,41 @@ func TestIWDBackend_MapIwdDBusError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIWDBackend_ApplyManagedObjects_DeviceWithoutStation(t *testing.T) {
|
||||||
|
backend, _ := NewIWDBackend()
|
||||||
|
|
||||||
|
err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
|
||||||
|
"/net/connman/iwd/0/1": {
|
||||||
|
iwdDeviceInterface: {
|
||||||
|
"Name": dbus.MakeVariant("wlan0"),
|
||||||
|
"Powered": dbus.MakeVariant(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, dbus.ObjectPath("/net/connman/iwd/0/1"), backend.devicePath)
|
||||||
|
assert.Empty(t, backend.stationPath)
|
||||||
|
|
||||||
|
state, err := backend.GetCurrentState()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "wlan0", state.WiFiDevice)
|
||||||
|
assert.False(t, state.WiFiEnabled)
|
||||||
|
assert.False(t, state.WiFiConnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIWDBackend_ApplyManagedObjects_NoDevice(t *testing.T) {
|
||||||
|
backend, _ := NewIWDBackend()
|
||||||
|
|
||||||
|
err := backend.applyManagedObjects(map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
|
||||||
|
"/net/connman/iwd/0": {
|
||||||
|
iwdAdapterInterface: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.ErrorContains(t, err, "no WiFi device found")
|
||||||
|
}
|
||||||
|
|
||||||
func TestIWDSavedWiFiProfilesFromManagedObjects(t *testing.T) {
|
func TestIWDSavedWiFiProfilesFromManagedObjects(t *testing.T) {
|
||||||
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
|
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
|
||||||
"/net/connman/iwd/known_network/1": {
|
"/net/connman/iwd/known_network/1": {
|
||||||
|
|||||||
@@ -42,12 +42,15 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
|
|||||||
for _, id := range installedNames {
|
for _, id := range installedNames {
|
||||||
if plugin, ok := pluginMap[id]; ok {
|
if plugin, ok := pluginMap[id]; ok {
|
||||||
hasUpdate := false
|
hasUpdate := false
|
||||||
if hasUpdates, err := manager.HasUpdates(id, plugin); err == nil {
|
diffURL := plugin.Repo
|
||||||
|
if hasUpdates, dURL, err := manager.HasUpdates(id, plugin); err == nil {
|
||||||
hasUpdate = hasUpdates
|
hasUpdate = hasUpdates
|
||||||
|
diffURL = dURL
|
||||||
}
|
}
|
||||||
|
|
||||||
info := pluginInfoFromPlugin(plugin)
|
info := pluginInfoFromPlugin(plugin)
|
||||||
info.HasUpdate = hasUpdate
|
info.HasUpdate = hasUpdate
|
||||||
|
info.DiffURL = diffURL
|
||||||
result = append(result, info)
|
result = append(result, info)
|
||||||
} else {
|
} else {
|
||||||
result = append(result, PluginInfo{
|
result = append(result, PluginInfo{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type PluginInfo struct {
|
|||||||
Note string `json:"note,omitempty"`
|
Note string `json:"note,omitempty"`
|
||||||
HasUpdate bool `json:"hasUpdate,omitempty"`
|
HasUpdate bool `json:"hasUpdate,omitempty"`
|
||||||
RequiresDMS string `json:"requires_dms,omitempty"`
|
RequiresDMS string `json:"requires_dms,omitempty"`
|
||||||
|
DiffURL string `json:"diffUrl,omitempty"`
|
||||||
Upvotes int `json:"upvotes,omitempty"`
|
Upvotes int `json:"upvotes,omitempty"`
|
||||||
Status []string `json:"status,omitempty"`
|
Status []string `json:"status,omitempty"`
|
||||||
IssueURL string `json:"issueUrl,omitempty"`
|
IssueURL string `json:"issueUrl,omitempty"`
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
serverThemes "github.com/AvengeMedia/DankMaterialShell/core/internal/server/themes"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wallpaper"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
)
|
)
|
||||||
@@ -56,6 +57,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(req.Method, "wallpaper.") {
|
||||||
|
if wallpaperManager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "wallpaper manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wallpaper.HandleRequest(conn, req, wallpaperManager)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(req.Method, "loginctl.") {
|
if strings.HasPrefix(req.Method, "loginctl.") {
|
||||||
if loginctlManager == nil {
|
if loginctlManager == nil {
|
||||||
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
models.RespondError(conn, req.ID, "loginctl manager not initialized")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
@@ -33,13 +34,14 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wallpaper"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const APIVersion = 26
|
const APIVersion = 27
|
||||||
|
|
||||||
var CLIVersion = "dev"
|
var CLIVersion = "dev"
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ var clipboardManager *clipboard.Manager
|
|||||||
var dbusManager *serverDbus.Manager
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
var themeModeManager *thememode.Manager
|
var themeModeManager *thememode.Manager
|
||||||
|
var wallpaperManager *wallpaper.Manager
|
||||||
var trayRecoveryManager *trayrecovery.Manager
|
var trayRecoveryManager *trayrecovery.Manager
|
||||||
var locationManager *location.Manager
|
var locationManager *location.Manager
|
||||||
var sysUpdateManager *sysupdate.Manager
|
var sysUpdateManager *sysupdate.Manager
|
||||||
@@ -188,6 +191,7 @@ func InitializeFreedeskManager() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
freedesktopManager = manager
|
freedesktopManager = manager
|
||||||
|
matugen.SetColorSchemeEchoHook(manager.ExpectColorSchemeEcho)
|
||||||
|
|
||||||
log.Info("Freedesktop manager initialized")
|
log.Info("Freedesktop manager initialized")
|
||||||
return nil
|
return nil
|
||||||
@@ -347,6 +351,13 @@ func InitializeThemeModeManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitializeWallpaperManager() error {
|
||||||
|
wallpaperManager = wallpaper.NewManager()
|
||||||
|
|
||||||
|
log.Info("Wallpaper rotation scheduler initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func InitializeTrayRecoveryManager() error {
|
func InitializeTrayRecoveryManager() error {
|
||||||
manager, err := trayrecovery.NewManager()
|
manager, err := trayrecovery.NewManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -463,6 +474,10 @@ func getCapabilities() Capabilities {
|
|||||||
caps = append(caps, "theme.auto")
|
caps = append(caps, "theme.auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wallpaperManager != nil {
|
||||||
|
caps = append(caps, "wallpaper")
|
||||||
|
}
|
||||||
|
|
||||||
if dbusManager != nil {
|
if dbusManager != nil {
|
||||||
caps = append(caps, "dbus")
|
caps = append(caps, "dbus")
|
||||||
}
|
}
|
||||||
@@ -529,6 +544,10 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "theme.auto")
|
caps = append(caps, "theme.auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wallpaperManager != nil {
|
||||||
|
caps = append(caps, "wallpaper")
|
||||||
|
}
|
||||||
|
|
||||||
if locationManager != nil {
|
if locationManager != nil {
|
||||||
caps = append(caps, "location")
|
caps = append(caps, "location")
|
||||||
}
|
}
|
||||||
@@ -841,6 +860,38 @@ func handleSubscribe(conn net.Conn, req models.Request) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if shouldSubscribe("wallpaper") && wallpaperManager != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
wallpaperChan := wallpaperManager.Subscribe(clientID + "-wallpaper")
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer wallpaperManager.Unsubscribe(clientID + "-wallpaper")
|
||||||
|
|
||||||
|
initialState := wallpaperManager.GetState()
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "wallpaper", Data: initialState}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state, ok := <-wallpaperChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case eventChan <- ServiceEvent{Service: "wallpaper", Data: state}:
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if shouldSubscribe("bluetooth") && bluezManager != nil {
|
if shouldSubscribe("bluetooth") && bluezManager != nil {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
bluezChan := bluezManager.Subscribe(clientID + "-bluetooth")
|
||||||
@@ -1286,6 +1337,9 @@ func cleanupManagers() {
|
|||||||
if themeModeManager != nil {
|
if themeModeManager != nil {
|
||||||
themeModeManager.Close()
|
themeModeManager.Close()
|
||||||
}
|
}
|
||||||
|
if wallpaperManager != nil {
|
||||||
|
wallpaperManager.Close()
|
||||||
|
}
|
||||||
if trayRecoveryManager != nil {
|
if trayRecoveryManager != nil {
|
||||||
trayRecoveryManager.Close()
|
trayRecoveryManager.Close()
|
||||||
}
|
}
|
||||||
@@ -1578,6 +1632,19 @@ func Start(printDocs bool) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := InitializeWallpaperManager(); err != nil {
|
||||||
|
log.Warnf("Wallpaper scheduler unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
notifyCapabilityChange()
|
||||||
|
go func() {
|
||||||
|
<-loginctlReady
|
||||||
|
if loginctlManager == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wallpaperManager.WatchLoginctl(loginctlManager)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-loginctlReady
|
<-loginctlReady
|
||||||
if loginctlManager == nil {
|
if loginctlManager == nil {
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
if manager == nil {
|
||||||
|
models.RespondError(conn, req.ID, "wallpaper manager not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Method {
|
||||||
|
case "wallpaper.getState":
|
||||||
|
handleGetState(conn, req, manager)
|
||||||
|
case "wallpaper.setConfig":
|
||||||
|
handleSetConfig(conn, req, manager)
|
||||||
|
case "wallpaper.trigger":
|
||||||
|
handleTrigger(conn, req, manager)
|
||||||
|
case "wallpaper.subscribe":
|
||||||
|
handleSubscribe(conn, req, manager)
|
||||||
|
default:
|
||||||
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
models.Respond(conn, req.ID, manager.GetState())
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetConfig(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
raw, ok := params.Any(req.Params, "config")
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "missing or invalid 'config' parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
|
models.RespondError(conn, req.ID, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetConfig(config)
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "wallpaper schedule set"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
manager.ResetSchedule(params.StringOpt(req.Params, "target", ""))
|
||||||
|
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "wallpaper schedule reset"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||||
|
clientID := fmt.Sprintf("client-%p", conn)
|
||||||
|
stateChan := manager.Subscribe(clientID)
|
||||||
|
defer manager.Unsubscribe(clientID)
|
||||||
|
|
||||||
|
initialState := manager.GetState()
|
||||||
|
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||||
|
ID: req.ID,
|
||||||
|
Result: &initialState,
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for state := range stateChan {
|
||||||
|
if err := json.NewEncoder(conn).Encode(models.Response[State]{
|
||||||
|
Result: &state,
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activeSchedule struct {
|
||||||
|
cfg ScheduleConfig
|
||||||
|
nextFire time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grace period before a missed-while-off schedule fires, so a freshly
|
||||||
|
// connected frontend has its cycle-seq baseline before the bump arrives.
|
||||||
|
var catchUpDelay = 3 * time.Second
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
config Config
|
||||||
|
configMutex sync.RWMutex
|
||||||
|
|
||||||
|
state *State
|
||||||
|
stateMutex sync.RWMutex
|
||||||
|
|
||||||
|
subscribers syncmap.Map[string, chan State]
|
||||||
|
|
||||||
|
stopChan chan struct{}
|
||||||
|
updateTrigger chan struct{}
|
||||||
|
resetReq chan string
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Owned by schedulerLoop after start; keyed like schedules ("" = global).
|
||||||
|
lastFires map[string]time.Time
|
||||||
|
persistFires func(map[string]time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return newManager(loadLastFires(), saveLastFires)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newManager(lastFires map[string]time.Time, persistFires func(map[string]time.Time)) *Manager {
|
||||||
|
if lastFires == nil {
|
||||||
|
lastFires = map[string]time.Time{}
|
||||||
|
}
|
||||||
|
m := &Manager{
|
||||||
|
config: Config{
|
||||||
|
Global: ScheduleConfig{Mode: "interval", IntervalSec: 300, Time: "06:00"},
|
||||||
|
Monitors: map[string]ScheduleConfig{},
|
||||||
|
},
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
updateTrigger: make(chan struct{}, 1),
|
||||||
|
resetReq: make(chan string, 8),
|
||||||
|
lastFires: lastFires,
|
||||||
|
persistFires: persistFires,
|
||||||
|
}
|
||||||
|
m.state = &State{Config: m.getConfig()}
|
||||||
|
|
||||||
|
m.wg.Add(1)
|
||||||
|
go m.schedulerLoop()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetState() State {
|
||||||
|
m.stateMutex.RLock()
|
||||||
|
defer m.stateMutex.RUnlock()
|
||||||
|
if m.state == nil {
|
||||||
|
return State{Config: m.getConfig()}
|
||||||
|
}
|
||||||
|
return *m.state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Subscribe(id string) chan State {
|
||||||
|
ch := make(chan State, 64)
|
||||||
|
m.subscribers.Store(id, ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Unsubscribe(id string) {
|
||||||
|
if val, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||||
|
close(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetConfig(config Config) {
|
||||||
|
if config.Monitors == nil {
|
||||||
|
config.Monitors = map[string]ScheduleConfig{}
|
||||||
|
}
|
||||||
|
m.configMutex.Lock()
|
||||||
|
if reflect.DeepEqual(m.config, config) {
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.config = config
|
||||||
|
m.configMutex.Unlock()
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ResetSchedule(target string) {
|
||||||
|
select {
|
||||||
|
case m.resetReq <- target:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TriggerUpdate() {
|
||||||
|
select {
|
||||||
|
case m.updateTrigger <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(m.stopChan)
|
||||||
|
}
|
||||||
|
m.wg.Wait()
|
||||||
|
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||||
|
close(ch)
|
||||||
|
m.subscribers.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
|
||||||
|
ch := lm.Subscribe("wallpaper")
|
||||||
|
m.wg.Go(func() {
|
||||||
|
defer lm.Unsubscribe("wallpaper")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
case state, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.PreparingForSleep {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) schedulerLoop() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
|
||||||
|
schedules := map[string]*activeSchedule{}
|
||||||
|
resets := map[string]bool{}
|
||||||
|
var seq uint64
|
||||||
|
var timer *time.Timer
|
||||||
|
|
||||||
|
for {
|
||||||
|
now := time.Now()
|
||||||
|
config := m.getConfig()
|
||||||
|
active := activeSchedules(config)
|
||||||
|
|
||||||
|
for key := range schedules {
|
||||||
|
if _, ok := active[key]; !ok {
|
||||||
|
delete(schedules, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firesDirty := false
|
||||||
|
for key, cfg := range active {
|
||||||
|
s, ok := schedules[key]
|
||||||
|
switch {
|
||||||
|
case !ok:
|
||||||
|
s = &activeSchedule{cfg: cfg, nextFire: computeNext(now, cfg)}
|
||||||
|
schedules[key] = s
|
||||||
|
if cfg.Mode == "time" {
|
||||||
|
last := m.lastFires[key]
|
||||||
|
prev, valid := prevDailyTime(now, cfg.Time)
|
||||||
|
switch {
|
||||||
|
case last.IsZero():
|
||||||
|
// First enable (or no history): seed instead of firing.
|
||||||
|
m.lastFires[key] = now
|
||||||
|
firesDirty = true
|
||||||
|
case valid && last.Before(prev):
|
||||||
|
// Scheduled time passed while dms wasn't running.
|
||||||
|
s.nextFire = now.Add(catchUpDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case s.cfg != cfg || resets[key]:
|
||||||
|
s.cfg = cfg
|
||||||
|
s.nextFire = computeNext(now, cfg)
|
||||||
|
}
|
||||||
|
delete(resets, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dueKeys []string
|
||||||
|
for key, s := range schedules {
|
||||||
|
if !s.nextFire.After(now) {
|
||||||
|
dueKeys = append(dueKeys, key)
|
||||||
|
s.nextFire = computeNext(now, s.cfg)
|
||||||
|
if s.cfg.Mode == "time" {
|
||||||
|
m.lastFires[key] = now
|
||||||
|
firesDirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firesDirty {
|
||||||
|
for key := range m.lastFires {
|
||||||
|
if _, ok := schedules[key]; !ok {
|
||||||
|
delete(m.lastFires, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.persistFires(m.lastFires)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, hasNext := soonest(schedules)
|
||||||
|
if len(dueKeys) == 0 {
|
||||||
|
m.setState(config, next, seq, "")
|
||||||
|
}
|
||||||
|
for _, key := range dueKeys {
|
||||||
|
seq++
|
||||||
|
m.setState(config, next, seq, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
waitDur := 24 * time.Hour
|
||||||
|
if hasNext {
|
||||||
|
waitDur = max(time.Until(next), time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if timer != nil {
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
timer = time.NewTimer(waitDur)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
timer.Stop()
|
||||||
|
return
|
||||||
|
case <-m.updateTrigger:
|
||||||
|
timer.Stop()
|
||||||
|
case key := <-m.resetReq:
|
||||||
|
timer.Stop()
|
||||||
|
resets[key] = true
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) setState(config Config, next time.Time, seq uint64, target string) {
|
||||||
|
newState := State{Config: config, NextRotation: next, CycleSeq: seq, Target: target}
|
||||||
|
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
if m.state != nil && statesEqual(m.state, &newState) {
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.state = &newState
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
|
m.notifySubscribers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) notifySubscribers() {
|
||||||
|
state := m.GetState()
|
||||||
|
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||||
|
select {
|
||||||
|
case ch <- state:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getConfig() Config {
|
||||||
|
m.configMutex.RLock()
|
||||||
|
defer m.configMutex.RUnlock()
|
||||||
|
return m.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeSchedules(config Config) map[string]ScheduleConfig {
|
||||||
|
out := map[string]ScheduleConfig{}
|
||||||
|
if config.PerMonitor {
|
||||||
|
for name, cfg := range config.Monitors {
|
||||||
|
if cfg.Enabled {
|
||||||
|
out[name] = cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
if config.Global.Enabled {
|
||||||
|
out[""] = config.Global
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeNext(now time.Time, cfg ScheduleConfig) time.Time {
|
||||||
|
switch cfg.Mode {
|
||||||
|
case "time":
|
||||||
|
return nextDailyTime(now, cfg.Time)
|
||||||
|
default:
|
||||||
|
sec := max(cfg.IntervalSec, 1)
|
||||||
|
return now.Add(time.Duration(sec) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextDailyTime(now time.Time, hhmm string) time.Time {
|
||||||
|
hour, minute, ok := parseHHMM(hhmm)
|
||||||
|
if !ok {
|
||||||
|
return now.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
|
||||||
|
if !next.After(now) {
|
||||||
|
next = next.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
func prevDailyTime(now time.Time, hhmm string) (time.Time, bool) {
|
||||||
|
hour, minute, ok := parseHHMM(hhmm)
|
||||||
|
if !ok {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
prev := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
|
||||||
|
if prev.After(now) {
|
||||||
|
prev = prev.Add(-24 * time.Hour)
|
||||||
|
}
|
||||||
|
return prev, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHHMM(hhmm string) (int, int, bool) {
|
||||||
|
parts := strings.Split(hhmm, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
hour, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil || hour < 0 || hour > 23 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
minute, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil || minute < 0 || minute > 59 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
return hour, minute, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func soonest(schedules map[string]*activeSchedule) (time.Time, bool) {
|
||||||
|
var best time.Time
|
||||||
|
found := false
|
||||||
|
for _, s := range schedules {
|
||||||
|
if !found || s.nextFire.Before(best) {
|
||||||
|
best = s.nextFire
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func statesEqual(a, b *State) bool {
|
||||||
|
switch {
|
||||||
|
case a == nil || b == nil:
|
||||||
|
return a == b
|
||||||
|
case a.CycleSeq != b.CycleSeq:
|
||||||
|
return false
|
||||||
|
case a.Target != b.Target:
|
||||||
|
return false
|
||||||
|
case !a.NextRotation.Equal(b.NextRotation):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(a.Config, b.Config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseHHMM(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in string
|
||||||
|
hour int
|
||||||
|
minute int
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"06:00", 6, 0, true},
|
||||||
|
{"23:59", 23, 59, true},
|
||||||
|
{"00:00", 0, 0, true},
|
||||||
|
{"24:00", 0, 0, false},
|
||||||
|
{"6:5", 6, 5, true},
|
||||||
|
{"bad", 0, 0, false},
|
||||||
|
{"12", 0, 0, false},
|
||||||
|
{"12:60", 0, 0, false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
hour, minute, ok := parseHHMM(c.in)
|
||||||
|
if ok != c.ok || (ok && (hour != c.hour || minute != c.minute)) {
|
||||||
|
t.Errorf("parseHHMM(%q) = (%d, %d, %v), want (%d, %d, %v)", c.in, hour, minute, ok, c.hour, c.minute, c.ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeNextInterval(t *testing.T) {
|
||||||
|
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
|
||||||
|
got := computeNext(now, ScheduleConfig{Mode: "interval", IntervalSec: 300})
|
||||||
|
if want := now.Add(300 * time.Second); !got.Equal(want) {
|
||||||
|
t.Errorf("interval next = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
clamped := computeNext(now, ScheduleConfig{Mode: "interval", IntervalSec: 0})
|
||||||
|
if want := now.Add(time.Second); !clamped.Equal(want) {
|
||||||
|
t.Errorf("interval clamp = %v, want %v", clamped, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComputeNextTime(t *testing.T) {
|
||||||
|
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
later := computeNext(now, ScheduleConfig{Mode: "time", Time: "18:00"})
|
||||||
|
if want := time.Date(2026, 6, 30, 18, 0, 0, 0, time.UTC); !later.Equal(want) {
|
||||||
|
t.Errorf("time next (today) = %v, want %v", later, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
tomorrow := computeNext(now, ScheduleConfig{Mode: "time", Time: "06:00"})
|
||||||
|
if want := time.Date(2026, 7, 1, 6, 0, 0, 0, time.UTC); !tomorrow.Equal(want) {
|
||||||
|
t.Errorf("time next (tomorrow) = %v, want %v", tomorrow, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActiveSchedules(t *testing.T) {
|
||||||
|
global := activeSchedules(Config{
|
||||||
|
Global: ScheduleConfig{Enabled: true, Mode: "interval", IntervalSec: 60},
|
||||||
|
})
|
||||||
|
if len(global) != 1 {
|
||||||
|
t.Fatalf("global active = %d, want 1", len(global))
|
||||||
|
}
|
||||||
|
if _, ok := global[""]; !ok {
|
||||||
|
t.Errorf("global active missing global key")
|
||||||
|
}
|
||||||
|
|
||||||
|
perMonitor := activeSchedules(Config{
|
||||||
|
PerMonitor: true,
|
||||||
|
Global: ScheduleConfig{Enabled: true},
|
||||||
|
Monitors: map[string]ScheduleConfig{
|
||||||
|
"DP-1": {Enabled: true, Mode: "interval", IntervalSec: 60},
|
||||||
|
"DP-2": {Enabled: false},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if len(perMonitor) != 1 {
|
||||||
|
t.Fatalf("per-monitor active = %d, want 1", len(perMonitor))
|
||||||
|
}
|
||||||
|
if _, ok := perMonitor["DP-1"]; !ok {
|
||||||
|
t.Errorf("per-monitor active missing DP-1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrevDailyTime(t *testing.T) {
|
||||||
|
now := time.Date(2026, 6, 30, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
earlier, ok := prevDailyTime(now, "06:00")
|
||||||
|
if !ok || !earlier.Equal(time.Date(2026, 6, 30, 6, 0, 0, 0, time.UTC)) {
|
||||||
|
t.Errorf("prev (today) = %v, %v", earlier, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
later, ok := prevDailyTime(now, "18:00")
|
||||||
|
if !ok || !later.Equal(time.Date(2026, 6, 29, 18, 0, 0, 0, time.UTC)) {
|
||||||
|
t.Errorf("prev (yesterday) = %v, %v", later, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := prevDailyTime(now, "bad"); ok {
|
||||||
|
t.Error("prev (invalid) should not be ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeScheduleCatchUpAfterRestart(t *testing.T) {
|
||||||
|
origDelay := catchUpDelay
|
||||||
|
catchUpDelay = 100 * time.Millisecond
|
||||||
|
defer func() { catchUpDelay = origDelay }()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
hhmm := now.Add(-2 * time.Hour).Format("15:04")
|
||||||
|
seed := map[string]time.Time{"": now.Add(-26 * time.Hour)}
|
||||||
|
|
||||||
|
m := newManager(seed, func(map[string]time.Time) {})
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
sub := m.Subscribe("test")
|
||||||
|
defer m.Unsubscribe("test")
|
||||||
|
|
||||||
|
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "time", Time: hhmm}})
|
||||||
|
|
||||||
|
deadline := time.After(3 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state := <-sub:
|
||||||
|
if state.CycleSeq > 0 && state.Target == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-deadline:
|
||||||
|
t.Fatal("missed time schedule did not catch up within 3s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeScheduleNoFireOnFirstEnable(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
hhmm := now.Add(-2 * time.Hour).Format("15:04")
|
||||||
|
|
||||||
|
m := newManager(map[string]time.Time{}, func(map[string]time.Time) {})
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
sub := m.Subscribe("test")
|
||||||
|
defer m.Unsubscribe("test")
|
||||||
|
|
||||||
|
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "time", Time: hhmm}})
|
||||||
|
|
||||||
|
deadline := time.After(1500 * time.Millisecond)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state := <-sub:
|
||||||
|
if state.CycleSeq > 0 {
|
||||||
|
t.Fatal("first enable must not fire immediately")
|
||||||
|
}
|
||||||
|
case <-deadline:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchedulerEmitsCycle(t *testing.T) {
|
||||||
|
m := newManager(map[string]time.Time{}, func(map[string]time.Time) {})
|
||||||
|
defer m.Close()
|
||||||
|
|
||||||
|
sub := m.Subscribe("test")
|
||||||
|
defer m.Unsubscribe("test")
|
||||||
|
|
||||||
|
m.SetConfig(Config{Global: ScheduleConfig{Enabled: true, Mode: "interval", IntervalSec: 1}})
|
||||||
|
|
||||||
|
deadline := time.After(3 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case state := <-sub:
|
||||||
|
if state.CycleSeq > 0 && state.Target == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-deadline:
|
||||||
|
t.Fatal("scheduler did not emit a cycle event within 3s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type persistentState struct {
|
||||||
|
LastFires map[string]time.Time `json:"lastFires"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateFilePath() string {
|
||||||
|
return filepath.Join(utils.XDGCacheHome(), "dms", "wallpaper-schedule.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadLastFires() map[string]time.Time {
|
||||||
|
data, err := os.ReadFile(stateFilePath())
|
||||||
|
if err != nil {
|
||||||
|
return map[string]time.Time{}
|
||||||
|
}
|
||||||
|
var state persistentState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil || state.LastFires == nil {
|
||||||
|
return map[string]time.Time{}
|
||||||
|
}
|
||||||
|
return state.LastFires
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveLastFires(fires map[string]time.Time) {
|
||||||
|
path := stateFilePath()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(persistentState{LastFires: fires})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||||
|
log.Warnf("wallpaper: failed to persist schedule state: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package wallpaper
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ScheduleConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
IntervalSec int `json:"intervalSec"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PerMonitor bool `json:"perMonitor"`
|
||||||
|
Global ScheduleConfig `json:"global"`
|
||||||
|
Monitors map[string]ScheduleConfig `json:"monitors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Config Config `json:"config"`
|
||||||
|
NextRotation time.Time `json:"nextRotation"`
|
||||||
|
CycleSeq uint64 `json:"cycleSeq"`
|
||||||
|
Target string `json:"target"`
|
||||||
|
}
|
||||||
@@ -74,6 +74,9 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
|||||||
|
|
||||||
if config.Enabled {
|
if config.Enabled {
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
|
if m.controlsInitialized {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Info("Gamma control enabled at startup")
|
log.Info("Gamma control enabled at startup")
|
||||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||||
m.availOutputsMu.RLock()
|
m.availOutputsMu.RLock()
|
||||||
@@ -184,14 +187,27 @@ func (m *Manager) setupRegistry() error {
|
|||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
|
|
||||||
if enabled && m.controlsInitialized {
|
if !enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
if err := m.addOutputControl(output); err != nil {
|
if err := m.addOutputControl(output); err != nil {
|
||||||
log.Warnf("Failed to add output control: %v", err)
|
log.Warnf("gamma: failed to add output control: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if m.controlsInitialized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// All outputs had been torn down (monitor sleep/disconnect),
|
||||||
|
// clearing controlsInitialized. Mark it ready again so the
|
||||||
|
// gamma_size event that follows control creation drives the
|
||||||
|
// reapply, instead of waiting for a manual toggle. No explicit
|
||||||
|
// apply here: applyGamma's dedup would suppress a no-change
|
||||||
|
// write anyway, and the new control isn't ready until gamma_size.
|
||||||
|
log.Info("gamma: output returned, re-establishing controls")
|
||||||
|
m.controlsInitialized = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
|
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
|
||||||
|
|||||||
@@ -108,10 +108,7 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
err := fmt.Errorf("FATAL: Wayland event dispatcher panic: %v", r)
|
err := fmt.Errorf("FATAL: Wayland event dispatcher panic: %v", r)
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
select {
|
sc.signalFatal(err)
|
||||||
case sc.fatalError <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -141,6 +138,7 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
continue
|
continue
|
||||||
case err != nil:
|
case err != nil:
|
||||||
log.Errorf("Poll error: %v", err)
|
log.Errorf("Poll error: %v", err)
|
||||||
|
sc.signalFatal(fmt.Errorf("wayland poll error: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +159,7 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
|
|
||||||
if consecutiveErrors >= maxConsecutiveErrors {
|
if consecutiveErrors >= maxConsecutiveErrors {
|
||||||
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
|
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
|
||||||
|
sc.signalFatal(fmt.Errorf("wayland connection unrecoverable after %d dispatch failures: %w", maxConsecutiveErrors, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +171,13 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *SharedContext) signalFatal(err error) {
|
||||||
|
select {
|
||||||
|
case sc.fatalError <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (sc *SharedContext) drainCmdQueue() {
|
func (sc *SharedContext) drainCmdQueue() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppChecker interface {
|
type AppChecker interface {
|
||||||
@@ -31,7 +34,20 @@ func (DefaultAppChecker) AnyFlatpakExists(flatpaks ...string) bool {
|
|||||||
|
|
||||||
func CommandExists(cmd string) bool {
|
func CommandExists(cmd string) bool {
|
||||||
_, err := exec.LookPath(cmd)
|
_, err := exec.LookPath(cmd)
|
||||||
return err == nil
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.ContainsRune(cmd, os.PathSeparator) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, dir := range userBinDirs() {
|
||||||
|
path := filepath.Join(dir, cmd)
|
||||||
|
info, statErr := os.Stat(path)
|
||||||
|
if statErr == nil && !info.IsDir() && info.Mode()&0o111 != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func AnyCommandExists(cmds ...string) bool {
|
func AnyCommandExists(cmds ...string) bool {
|
||||||
@@ -42,3 +58,58 @@ func AnyCommandExists(cmds ...string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EnvWithUserBinPath(env []string) []string {
|
||||||
|
if env == nil {
|
||||||
|
env = os.Environ()
|
||||||
|
}
|
||||||
|
|
||||||
|
out := append([]string(nil), env...)
|
||||||
|
pathIndex := -1
|
||||||
|
pathValue := ""
|
||||||
|
for i, entry := range out {
|
||||||
|
if strings.HasPrefix(entry, "PATH=") {
|
||||||
|
pathIndex = i
|
||||||
|
pathValue = strings.TrimPrefix(entry, "PATH=")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := filepath.SplitList(pathValue)
|
||||||
|
seen := make(map[string]struct{}, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
if part != "" {
|
||||||
|
seen[part] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepend := make([]string, 0, 2)
|
||||||
|
for _, dir := range userBinDirs() {
|
||||||
|
if dir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[dir]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prepend = append(prepend, dir)
|
||||||
|
seen[dir] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(prepend, parts...)
|
||||||
|
newPath := "PATH=" + strings.Join(parts, string(os.PathListSeparator))
|
||||||
|
if pathIndex >= 0 {
|
||||||
|
out[pathIndex] = newPath
|
||||||
|
} else {
|
||||||
|
out = append(out, newPath)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func userBinDirs() []string {
|
||||||
|
dirs := []string{}
|
||||||
|
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
||||||
|
dirs = append(dirs, filepath.Join(home, ".local", "bin"))
|
||||||
|
}
|
||||||
|
dirs = append(dirs, "/usr/local/bin")
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommandExistsFallsBackToLocalBin(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
binDir := filepath.Join(home, ".local", "bin")
|
||||||
|
require.NoError(t, os.MkdirAll(binDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(binDir, "pywalfox"), []byte("#!/bin/sh\n"), 0o755))
|
||||||
|
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("PATH", t.TempDir())
|
||||||
|
|
||||||
|
assert.True(t, CommandExists("pywalfox"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandExistsIgnoresNonExecutableLocalBinFile(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
binDir := filepath.Join(home, ".local", "bin")
|
||||||
|
require.NoError(t, os.MkdirAll(binDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(binDir, "pywalfox"), []byte("not executable"), 0o644))
|
||||||
|
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("PATH", t.TempDir())
|
||||||
|
|
||||||
|
assert.False(t, CommandExists("pywalfox"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvWithUserBinPathPrependsLocalBin(t *testing.T) {
|
||||||
|
home := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
|
||||||
|
env := EnvWithUserBinPath([]string{"PATH=/usr/bin", "OTHER=value"})
|
||||||
|
var pathValue string
|
||||||
|
for _, entry := range env {
|
||||||
|
if strings.HasPrefix(entry, "PATH=") {
|
||||||
|
pathValue = strings.TrimPrefix(entry, "PATH=")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := filepath.SplitList(pathValue)
|
||||||
|
require.NotEmpty(t, parts)
|
||||||
|
assert.Equal(t, filepath.Join(home, ".local", "bin"), parts[0])
|
||||||
|
assert.Contains(t, parts, "/usr/local/bin")
|
||||||
|
assert.Contains(t, env, "OTHER=value")
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "golang.org/x/exp/constraints"
|
import "cmp"
|
||||||
|
|
||||||
func Clamp[T constraints.Ordered](val, min, max T) T {
|
func Clamp[T cmp.Ordered](val, min, max T) T {
|
||||||
if val < min {
|
if val < min {
|
||||||
return min
|
return min
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ override_dh_auto_install:
|
|||||||
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
|
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
|
||||||
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
|
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
|
||||||
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
|
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
|
||||||
|
install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \
|
||||||
install -Dm644 assets/danklogo.svg debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
|
install -Dm644 assets/danklogo.svg debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
|
||||||
else \
|
else \
|
||||||
echo "ERROR: quickshell directory not found!" && \
|
echo "ERROR: quickshell directory not found!" && \
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ override_dh_auto_install:
|
|||||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
|
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
|
||||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
|
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
|
||||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
|
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
|
||||||
|
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \
|
||||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/danklogo.svg debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
|
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/danklogo.svg debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg; \
|
||||||
else \
|
else \
|
||||||
echo "ERROR: DankMaterialShell-$(UPSTREAM_VERSION) directory not found!" && \
|
echo "ERROR: DankMaterialShell-$(UPSTREAM_VERSION) directory not found!" && \
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.servic
|
|||||||
|
|
||||||
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
install -Dm644 assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
# Install shell files to shared data location
|
# Install shell files to shared data location
|
||||||
@@ -146,6 +147,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
%files -n dms-cli
|
%files -n dms-cli
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_us
|
|||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
install -Dm644 %{_builddir}/dms-qml/assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
@@ -113,6 +114,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
%files -n dms-cli
|
%files -n dms-cli
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ in
|
|||||||
]
|
]
|
||||||
++ lib.optional cfg.enableDynamicTheming pkgs.matugen
|
++ lib.optional cfg.enableDynamicTheming pkgs.matugen
|
||||||
++ lib.optional cfg.enableAudioWavelength pkgs.cava
|
++ lib.optional cfg.enableAudioWavelength pkgs.cava
|
||||||
++ lib.optional cfg.enableCalendarEvents pkgs.khal
|
++ lib.optional cfg.enableCalendarEvents pkgs.khal;
|
||||||
++ lib.optional cfg.enableClipboardPaste pkgs.wtype;
|
|
||||||
|
|
||||||
plugins = lib.mapAttrs (name: plugin: {
|
plugins = lib.mapAttrs (name: plugin: {
|
||||||
source = plugin.src;
|
source = plugin.src;
|
||||||
|
|||||||
+49
-1
@@ -10,6 +10,8 @@ let
|
|||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
cfg = config.programs.dank-material-shell.greeter;
|
cfg = config.programs.dank-material-shell.greeter;
|
||||||
cfgDms = config.programs.dank-material-shell;
|
cfgDms = config.programs.dank-material-shell;
|
||||||
|
cfgAutoLogin = config.services.displayManager.autoLogin;
|
||||||
|
sessionData = config.services.displayManager.sessionData;
|
||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
inherit (config.services.greetd.settings.default_session) user;
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ let
|
|||||||
cfg.quickshell.package
|
cfg.quickshell.package
|
||||||
compositorPackage
|
compositorPackage
|
||||||
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
||||||
|
pkgs.jq # reads the user's cursor theme from settings.json in dms-greeter
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@@ -48,6 +51,38 @@ let
|
|||||||
} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
|
} ${lib.optionalString cfg.logs.save "> ${cfg.logs.path} 2>&1"}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
autoLoginCommand =
|
||||||
|
pkgs.runCommand "dms-greeter-autologin-command"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.gnugrep
|
||||||
|
pkgs.coreutils
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
session="${sessionData.autologinSession}"
|
||||||
|
desktops="${sessionData.desktops}"
|
||||||
|
|
||||||
|
for sessionFile in \
|
||||||
|
"$desktops/share/wayland-sessions/$session.desktop" \
|
||||||
|
"$desktops/share/xsessions/$session.desktop"
|
||||||
|
do
|
||||||
|
if [ -f "$sessionFile" ]; then
|
||||||
|
command="$(grep -m1 '^Exec=' "$sessionFile" | cut -d= -f2- || true)"
|
||||||
|
|
||||||
|
if [ -n "$command" ]; then
|
||||||
|
printf '%s\n' "$command" > "$out"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "dms-greeter autologin: could not resolve Exec for session '$session'" >&2
|
||||||
|
exit 1
|
||||||
|
'';
|
||||||
|
|
||||||
jq = lib.getExe pkgs.jq;
|
jq = lib.getExe pkgs.jq;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -155,6 +190,13 @@ in
|
|||||||
dmsgreeter: user set for greetd default_session ${user} does not exist. Please create it before referencing it.
|
dmsgreeter: user set for greetd default_session ${user} does not exist. Please create it before referencing it.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion = cfgAutoLogin.enable -> sessionData.autologinSession != null;
|
||||||
|
message = ''
|
||||||
|
dms-greeter auto-login requires services.displayManager.defaultSession to be set,
|
||||||
|
or at least one session in services.displayManager.sessionPackages.
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
|
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
|
||||||
# Declare security.pam.services.dankshell only if you want to override that runtime fallback.
|
# Declare security.pam.services.dankshell only if you want to override that runtime fallback.
|
||||||
@@ -165,7 +207,13 @@ in
|
|||||||
# };
|
# };
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = lib.mkDefault true;
|
enable = lib.mkDefault true;
|
||||||
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
settings = {
|
||||||
|
default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
||||||
|
initial_session = lib.mkIf (cfgAutoLogin.enable && (cfgAutoLogin.user != null)) {
|
||||||
|
inherit (cfgAutoLogin) user;
|
||||||
|
command = ''${lib.getExe pkgs.bash} -lc "${pkgs.systemd}/bin/systemd-cat $(<${autoLoginCommand})"'';
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
fonts.packages = with pkgs; [
|
fonts.packages = with pkgs; [
|
||||||
fira-code
|
fira-code
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ in
|
|||||||
enableClipboardPaste = lib.mkOption {
|
enableClipboardPaste = lib.mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = "Adds needed dependencies for directly pasting items from the clipboard history.";
|
description = "Deprecated: paste is built into dms; no extra dependencies needed. Kept as a no-op for compatibility.";
|
||||||
};
|
};
|
||||||
|
|
||||||
quickshell = {
|
quickshell = {
|
||||||
|
|||||||
@@ -16,8 +16,14 @@ pkgs.testers.runNixOSTest {
|
|||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "greeter";
|
group = "greeter";
|
||||||
};
|
};
|
||||||
|
users.users.alice.isNormalUser = true;
|
||||||
|
|
||||||
services.greetd.settings.default_session.user = "greeter";
|
services.greetd.settings.default_session.user = "greeter";
|
||||||
|
services.displayManager.autoLogin = {
|
||||||
|
enable = true;
|
||||||
|
user = "alice";
|
||||||
|
};
|
||||||
|
services.displayManager.defaultSession = "niri";
|
||||||
|
|
||||||
programs.niri.enable = true;
|
programs.niri.enable = true;
|
||||||
|
|
||||||
@@ -46,6 +52,11 @@ pkgs.testers.runNixOSTest {
|
|||||||
greetd_config_path = config_match.group(1)
|
greetd_config_path = config_match.group(1)
|
||||||
greetd_config = machine.succeed(f"cat {greetd_config_path}")
|
greetd_config = machine.succeed(f"cat {greetd_config_path}")
|
||||||
t.assertIn("dms-greeter", greetd_config)
|
t.assertIn("dms-greeter", greetd_config)
|
||||||
|
t.assertIn("[initial_session]", greetd_config)
|
||||||
|
|
||||||
|
initial_session = greetd_config.split("[initial_session]", 1)[1]
|
||||||
|
t.assertIn('user = "alice"', initial_session)
|
||||||
|
t.assertIn("systemd-cat", initial_session)
|
||||||
|
|
||||||
script_match = re.search(r'command\s*=\s*"([^"]+/bin/dms-greeter)"', greetd_config)
|
script_match = re.search(r'command\s*=\s*"([^"]+/bin/dms-greeter)"', greetd_config)
|
||||||
if script_match is None:
|
if script_match is None:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Recommends: danksearch
|
|||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: quickshell-git
|
Recommends: quickshell-git
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-multimedia-imports
|
||||||
Suggests: cups-pk-helper
|
Suggests: cups-pk-helper
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|
||||||
@@ -117,6 +117,7 @@ install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.servic
|
|||||||
|
|
||||||
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
install -Dm644 assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
@@ -153,6 +154,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
%dir %{_datadir}/icons/hicolor
|
%dir %{_datadir}/icons/hicolor
|
||||||
%dir %{_datadir}/icons/hicolor/scalable
|
%dir %{_datadir}/icons/hicolor/scalable
|
||||||
%dir %{_datadir}/icons/hicolor/scalable/apps
|
%dir %{_datadir}/icons/hicolor/scalable/apps
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Recommends: cava
|
|||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-multimedia-imports
|
||||||
Suggests: cups-pk-helper
|
Suggests: cups-pk-helper
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.servic
|
|||||||
|
|
||||||
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
install -Dm644 assets/com.danklinux.dms.notepad.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
@@ -100,6 +101,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
|
|||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
%{_datadir}/applications/dms-open.desktop
|
%{_datadir}/applications/dms-open.desktop
|
||||||
%{_datadir}/applications/com.danklinux.dms.desktop
|
%{_datadir}/applications/com.danklinux.dms.desktop
|
||||||
|
%{_datadir}/applications/com.danklinux.dms.notepad.desktop
|
||||||
%dir %{_datadir}/icons/hicolor
|
%dir %{_datadir}/icons/hicolor
|
||||||
%dir %{_datadir}/icons/hicolor/scalable
|
%dir %{_datadir}/icons/hicolor/scalable
|
||||||
%dir %{_datadir}/icons/hicolor/scalable/apps
|
%dir %{_datadir}/icons/hicolor/scalable/apps
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ override_dh_auto_install:
|
|||||||
debian/dms-git/usr/share/applications/dms-open.desktop
|
debian/dms-git/usr/share/applications/dms-open.desktop
|
||||||
install -Dm644 dms-git-repo/assets/com.danklinux.dms.desktop \
|
install -Dm644 dms-git-repo/assets/com.danklinux.dms.desktop \
|
||||||
debian/dms-git/usr/share/applications/com.danklinux.dms.desktop
|
debian/dms-git/usr/share/applications/com.danklinux.dms.desktop
|
||||||
|
install -Dm644 dms-git-repo/assets/com.danklinux.dms.notepad.desktop \
|
||||||
|
debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop
|
||||||
install -Dm644 dms-git-repo/assets/danklogo.svg \
|
install -Dm644 dms-git-repo/assets/danklogo.svg \
|
||||||
debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg
|
debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ override_dh_auto_install:
|
|||||||
debian/dms/usr/share/applications/dms-open.desktop
|
debian/dms/usr/share/applications/dms-open.desktop
|
||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/com.danklinux.dms.desktop \
|
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/com.danklinux.dms.desktop \
|
||||||
debian/dms/usr/share/applications/com.danklinux.dms.desktop
|
debian/dms/usr/share/applications/com.danklinux.dms.desktop
|
||||||
|
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/com.danklinux.dms.notepad.desktop \
|
||||||
|
debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop
|
||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/danklogo.svg \
|
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/danklogo.svg \
|
||||||
debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg
|
debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
|||||||
+33
-9
@@ -13,16 +13,41 @@ All build from source.
|
|||||||
|
|
||||||
## Distribution
|
## Distribution
|
||||||
|
|
||||||
These packages target the official
|
This is a DMS maintained repo for VoidLinux until these packages are officially merged upstream in the Void Linux repositories, you can install them from our self-hosted custom XBPS repositories served via GitHub Pages.
|
||||||
[`void-linux/void-packages`](https://github.com/void-linux/void-packages)
|
|
||||||
repository, so they install with a plain `xbps-install dms` and no extra setup.
|
### Using the Self-Hosted Repositories
|
||||||
Most dependencies (`quickshell`, `matugen`, `cava`, `niri`, `greetd`, …) are
|
|
||||||
already in Void; `dgop` and `danksearch` are packaged alongside in the
|
We serve both stable release and development packages directly from our repository branches.
|
||||||
[danklinux repo](https://github.com/AvengeMedia/danklinux/tree/master/distro/void).
|
|
||||||
|
#### 1. Add Repository Configurations
|
||||||
|
|
||||||
|
Create configuration files in `/etc/xbps.d/` pointing to our repositories (needed for both stable and git/nightly variants):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "repository=https://avengemedia.github.io/DankMaterialShell/current" | sudo tee /etc/xbps.d/dms.conf
|
||||||
|
echo "repository=https://avengemedia.github.io/DankLinux/current" | sudo tee /etc/xbps.d/danklinux.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Install DMS
|
||||||
|
|
||||||
|
Synchronize repositories and install the package:
|
||||||
|
|
||||||
|
* For the **stable** variant:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo xbps-install -S dms
|
||||||
|
```
|
||||||
|
|
||||||
|
* For the **git/nightly** variant (this will conflict with and replace the stable package):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo xbps-install -S dms-git
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note: On the first sync, `xbps-install` will output our signing key fingerprint and ask you to type `y` to trust and import it. Verify that the key matches our official signing fingerprint.*
|
||||||
|
|
||||||
The templates here are the source of truth: copy each into a void-packages
|
The templates here are the source of truth: copy each into a void-packages
|
||||||
checkout at `srcpkgs/<pkg>/template` to build or submit it. Only tagged releases
|
checkout at `srcpkgs/<pkg>/template` to build or submit it.
|
||||||
are packaged (no `-git`/nightly variant).
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -36,7 +61,6 @@ install whichever features you want:
|
|||||||
| `cava` | audio visualiser widget |
|
| `cava` | audio visualiser widget |
|
||||||
| `qt6-multimedia` | system sound feedback |
|
| `qt6-multimedia` | system sound feedback |
|
||||||
| `qt6ct` | Qt app theming |
|
| `qt6ct` | Qt app theming |
|
||||||
| `wtype` | virtual keyboard input |
|
|
||||||
| `power-profiles-daemon` | power profile control |
|
| `power-profiles-daemon` | power profile control |
|
||||||
| `cups-pk-helper` | printer management |
|
| `cups-pk-helper` | printer management |
|
||||||
| `NetworkManager` | network control |
|
| `NetworkManager` | network control |
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Template file for 'dms-git'
|
||||||
|
#
|
||||||
|
# DankMaterialShell git development release
|
||||||
|
#
|
||||||
|
# NOTE: This template is built exclusively by CI. The workflow stages a local
|
||||||
|
# source tarball and dynamically patches version, checksum, and distfiles
|
||||||
|
# before calling xbps-src. Do not attempt to build this template manually
|
||||||
|
# without first providing the source archive.
|
||||||
|
#
|
||||||
|
# The binary is built with the `distro_binary` build tag (drops the in-app
|
||||||
|
# self-update command).
|
||||||
|
pkgname=dms-git
|
||||||
|
version=1.4.6.git
|
||||||
|
revision=1
|
||||||
|
create_wrksrc=yes
|
||||||
|
build_style=go
|
||||||
|
build_wrksrc="core"
|
||||||
|
go_import_path="github.com/AvengeMedia/DankMaterialShell/core"
|
||||||
|
go_package="${go_import_path}/cmd/dms"
|
||||||
|
go_build_tags="distro_binary"
|
||||||
|
go_ldflags="-X main.Version=${version}"
|
||||||
|
short_desc="DankMaterialShell — Material 3 desktop shell for Wayland (git variant)"
|
||||||
|
maintainer="AvengeMedia <AvengeMedia.US@gmail.com>"
|
||||||
|
license="MIT"
|
||||||
|
homepage="https://danklinux.com"
|
||||||
|
# CI populates distfiles/checksum dynamically; these are placeholders.
|
||||||
|
distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz"
|
||||||
|
checksum=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb
|
||||||
|
|
||||||
|
# Conflicts and provides
|
||||||
|
conflicts="dms"
|
||||||
|
provides="dms-${version}_${revision}"
|
||||||
|
|
||||||
|
# Optional feature deps are listed in distro/void/README.md.
|
||||||
|
depends="quickshell accountsservice dgop matugen dbus"
|
||||||
|
|
||||||
|
post_install() {
|
||||||
|
# QML shell tree (build_style=go already installed the dms binary)
|
||||||
|
vmkdir usr/share/quickshell/dms
|
||||||
|
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
|
||||||
|
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
|
||||||
|
|
||||||
|
# Desktop entry + icon
|
||||||
|
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||||
|
vinstall "${wrksrc}/assets/com.danklinux.dms.desktop" 644 usr/share/applications
|
||||||
|
vinstall "${wrksrc}/assets/com.danklinux.dms.notepad.desktop" 644 usr/share/applications
|
||||||
|
vinstall "${wrksrc}/assets/danklogo.svg" 644 usr/share/icons/hicolor/scalable/apps
|
||||||
|
|
||||||
|
# Shell completions (generated by the built binary; skip when cross-building)
|
||||||
|
vmkdir usr/share/bash-completion/completions
|
||||||
|
vmkdir usr/share/zsh/site-functions
|
||||||
|
vmkdir usr/share/fish/vendor_completions.d
|
||||||
|
if [ -z "$CROSS_BUILD" ]; then
|
||||||
|
"${DESTDIR}/usr/bin/dms" completion bash > "${DESTDIR}/usr/share/bash-completion/completions/dms"
|
||||||
|
"${DESTDIR}/usr/bin/dms" completion zsh > "${DESTDIR}/usr/share/zsh/site-functions/_dms"
|
||||||
|
"${DESTDIR}/usr/bin/dms" completion fish > "${DESTDIR}/usr/share/fish/vendor_completions.d/dms.fish"
|
||||||
|
fi
|
||||||
|
|
||||||
|
vlicense "${wrksrc}/LICENSE"
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ changelog="https://github.com/AvengeMedia/DankMaterialShell/releases"
|
|||||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz"
|
distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz"
|
||||||
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
|
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
|
||||||
|
|
||||||
# Optional feature deps (XBPS has no "recommends") are listed in distro/void/README.md.
|
# Optional feature deps are listed in distro/void/README.md.
|
||||||
depends="quickshell accountsservice dgop matugen"
|
depends="quickshell accountsservice dgop matugen dbus"
|
||||||
|
|
||||||
post_install() {
|
post_install() {
|
||||||
# QML shell tree (build_style=go already installed the dms binary)
|
# QML shell tree (build_style=go already installed the dms binary)
|
||||||
@@ -33,6 +33,7 @@ post_install() {
|
|||||||
# Desktop entry + icon
|
# Desktop entry + icon
|
||||||
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
|
||||||
vinstall "${wrksrc}/assets/com.danklinux.dms.desktop" 644 usr/share/applications
|
vinstall "${wrksrc}/assets/com.danklinux.dms.desktop" 644 usr/share/applications
|
||||||
|
vinstall "${wrksrc}/assets/com.danklinux.dms.notepad.desktop" 644 usr/share/applications
|
||||||
vinstall "${wrksrc}/assets/danklogo.svg" 644 usr/share/icons/hicolor/scalable/apps
|
vinstall "${wrksrc}/assets/danklogo.svg" 644 usr/share/icons/hicolor/scalable/apps
|
||||||
|
|
||||||
# Shell completions (generated by the built binary; skip when cross-building)
|
# Shell completions (generated by the built binary; skip when cross-building)
|
||||||
|
|||||||
@@ -684,6 +684,10 @@ Notepad/scratchpad modal control for quick note-taking.
|
|||||||
- `open` - Show notepad modal
|
- `open` - Show notepad modal
|
||||||
- `close` - Hide notepad modal
|
- `close` - Hide notepad modal
|
||||||
- `toggle` - Toggle notepad modal visibility
|
- `toggle` - Toggle notepad modal visibility
|
||||||
|
- `openFile <path>` - Show notepad and load a file into a tab, reusing an existing tab if that file is already open
|
||||||
|
- Parameters: `path` - Absolute path to a file. Empty path behaves like `open`.
|
||||||
|
- Returns: Success/failure message
|
||||||
|
- This is what `com.danklinux.dms.notepad.desktop`'s `Exec=dms ipc call notepad openFile %f` uses so Notepad shows up in "Open With" pickers for text files.
|
||||||
- `expand` - Expand the active notepad width and open it if hidden
|
- `expand` - Expand the active notepad width and open it if hidden
|
||||||
- `collapse` - Collapse the active notepad width without changing visibility
|
- `collapse` - Collapse the active notepad width without changing visibility
|
||||||
- `toggleExpand` - Toggle the active notepad width between collapsed and expanded
|
- `toggleExpand` - Toggle the active notepad width between collapsed and expanded
|
||||||
|
|||||||
@@ -133,11 +133,16 @@
|
|||||||
$out/share/applications/dms-open.desktop
|
$out/share/applications/dms-open.desktop
|
||||||
install -D ${rootSrc}/assets/com.danklinux.dms.desktop \
|
install -D ${rootSrc}/assets/com.danklinux.dms.desktop \
|
||||||
$out/share/applications/com.danklinux.dms.desktop
|
$out/share/applications/com.danklinux.dms.desktop
|
||||||
|
install -D ${rootSrc}/assets/com.danklinux.dms.notepad.desktop \
|
||||||
|
$out/share/applications/com.danklinux.dms.notepad.desktop
|
||||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
|
# Snapshot pre-wrap Qt paths for launched apps to restore.
|
||||||
wrapProgram $out/bin/dms \
|
wrapProgram $out/bin/dms \
|
||||||
--add-flags "-c $out/share/quickshell/dms" \
|
--add-flags "-c $out/share/quickshell/dms" \
|
||||||
|
--run 'export DMS_ORIG_NIXPKGS_QT6_QML_IMPORT_PATH="''${NIXPKGS_QT6_QML_IMPORT_PATH:-}"' \
|
||||||
|
--run 'export DMS_ORIG_QT_PLUGIN_PATH="''${QT_PLUGIN_PATH:-}"' \
|
||||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Singleton {
|
|||||||
|
|
||||||
readonly property bool isDirectionalEffect: isConnectedEffect || _effect === 1
|
readonly property bool isDirectionalEffect: isConnectedEffect || _effect === 1
|
||||||
readonly property bool isDepthEffect: _effect === 2
|
readonly property bool isDepthEffect: _effect === 2
|
||||||
readonly property bool isConnectedEffect: (typeof SettingsData !== "undefined") && SettingsData.connectedFrameModeActive
|
readonly property bool isConnectedEffect: (typeof FrameTransitionState !== "undefined") && FrameTransitionState.effectiveConnectedFrameModeActive
|
||||||
|
|
||||||
readonly property real effectScaleCollapsed: _effectScaleCollapsed[_effect] !== undefined ? _effectScaleCollapsed[_effect] : 0.96
|
readonly property real effectScaleCollapsed: _effectScaleCollapsed[_effect] !== undefined ? _effectScaleCollapsed[_effect] : 0.96
|
||||||
readonly property real effectAnimOffset: _effectAnimOffsets[_effect] !== undefined ? _effectAnimOffsets[_effect] : 16
|
readonly property real effectAnimOffset: _effectAnimOffsets[_effect] !== undefined ? _effectAnimOffsets[_effect] : 16
|
||||||
|
|||||||
@@ -7,6 +7,24 @@ function dirname(path) {
|
|||||||
return idx > 0 ? path.substring(0, idx) : ".";
|
return idx > 0 ? path.substring(0, idx) : ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sectionHeaderFor(includeLine) {
|
||||||
|
const line = String(includeLine ?? "").trim();
|
||||||
|
if (line.startsWith("require"))
|
||||||
|
return "-- DMS Include Configs";
|
||||||
|
if (line.startsWith("source"))
|
||||||
|
return "# DMS Include Configs";
|
||||||
|
return "// DMS Include Configs";
|
||||||
|
}
|
||||||
|
|
||||||
|
function managedIncludePatternFor(includeLine) {
|
||||||
|
const line = String(includeLine ?? "").trim();
|
||||||
|
if (line.startsWith("require"))
|
||||||
|
return "require.*dms[.]";
|
||||||
|
if (line.startsWith("source"))
|
||||||
|
return "source.*dms/";
|
||||||
|
return "include.*dms/";
|
||||||
|
}
|
||||||
|
|
||||||
function buildRepairScript(options) {
|
function buildRepairScript(options) {
|
||||||
const configFile = options.configFile;
|
const configFile = options.configFile;
|
||||||
const backupFile = options.backupFile;
|
const backupFile = options.backupFile;
|
||||||
@@ -31,7 +49,9 @@ function buildRepairScript(options) {
|
|||||||
for (const include of includes) {
|
for (const include of includes) {
|
||||||
if (!include.grepPattern || !include.includeLine)
|
if (!include.grepPattern || !include.includeLine)
|
||||||
continue;
|
continue;
|
||||||
commands.push(`if ! grep -v '^[[:space:]]*\\(//\\|#\\|--\\)' ${shQuote(configFile)} 2>/dev/null | grep -q ${shQuote(include.grepPattern)}; then echo '' >> ${shQuote(configFile)} && printf '%s\\n' ${shQuote(include.includeLine)} >> ${shQuote(configFile)}; fi`);
|
const sectionHeader = options.sectionHeader || sectionHeaderFor(include.includeLine);
|
||||||
|
const managedIncludePattern = managedIncludePatternFor(include.includeLine);
|
||||||
|
commands.push(`if ! grep -v '^[[:space:]]*\\(//\\|#\\|--\\)' ${shQuote(configFile)} 2>/dev/null | grep -q ${shQuote(include.grepPattern)}; then if grep -Fqx ${shQuote(sectionHeader)} ${shQuote(configFile)} 2>/dev/null || grep -v '^[[:space:]]*\\(//\\|#\\|--\\)' ${shQuote(configFile)} 2>/dev/null | grep -q ${shQuote(managedIncludePattern)}; then printf '%s\\n' ${shQuote(include.includeLine)} >> ${shQuote(configFile)}; elif [ -s ${shQuote(configFile)} ]; then printf '\\n%s\\n%s\\n' ${shQuote(sectionHeader)} ${shQuote(include.includeLine)} >> ${shQuote(configFile)}; else printf '%s\\n%s\\n' ${shQuote(sectionHeader)} ${shQuote(include.includeLine)} >> ${shQuote(configFile)}; fi; fi`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return commands.join("; ");
|
return commands.join("; ");
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int revision: 0
|
||||||
|
property int appliedRevision: 0
|
||||||
|
readonly property bool ready: appliedRevision >= revision
|
||||||
|
|
||||||
|
// Latched: surfaces render the last compositor-acknowledged state until the atomic flip on ack
|
||||||
|
property bool effectiveFrameEnabled: false
|
||||||
|
property string effectiveFrameMode: "connected"
|
||||||
|
readonly property bool effectiveConnectedFrameModeActive: effectiveFrameEnabled && effectiveFrameMode === "connected"
|
||||||
|
|
||||||
|
signal transitionRequested(int revision)
|
||||||
|
|
||||||
|
function begin() {
|
||||||
|
revision++;
|
||||||
|
transitionRequested(revision);
|
||||||
|
return revision;
|
||||||
|
}
|
||||||
|
|
||||||
|
function acknowledge(requestRevision) {
|
||||||
|
if (requestRevision > appliedRevision)
|
||||||
|
appliedRevision = requestRevision;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncEffective() {
|
||||||
|
effectiveFrameEnabled = SettingsData.frameEnabled;
|
||||||
|
effectiveFrameMode = SettingsData.frameMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
onReadyChanged: {
|
||||||
|
if (ready)
|
||||||
|
syncEffective();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks settings-load changes; live toggles begin() first (ready false) so the latch holds
|
||||||
|
Connections {
|
||||||
|
target: SettingsData
|
||||||
|
function onFrameEnabledChanged() {
|
||||||
|
if (root.ready)
|
||||||
|
root.syncEffective();
|
||||||
|
}
|
||||||
|
function onFrameModeChanged() {
|
||||||
|
if (root.ready)
|
||||||
|
root.syncEffective();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: syncEffective()
|
||||||
|
}
|
||||||
@@ -68,27 +68,6 @@ const KEY_MAP = {
|
|||||||
16777429: "XF86Explorer",
|
16777429: "XF86Explorer",
|
||||||
16777442: "XF86Launch0",
|
16777442: "XF86Launch0",
|
||||||
16777443: "XF86Launch1",
|
16777443: "XF86Launch1",
|
||||||
33: "1",
|
|
||||||
64: "2",
|
|
||||||
35: "3",
|
|
||||||
36: "4",
|
|
||||||
37: "5",
|
|
||||||
94: "6",
|
|
||||||
38: "7",
|
|
||||||
42: "8",
|
|
||||||
40: "9",
|
|
||||||
41: "0",
|
|
||||||
60: "Comma",
|
|
||||||
62: "Period",
|
|
||||||
63: "Slash",
|
|
||||||
58: "Semicolon",
|
|
||||||
34: "Apostrophe",
|
|
||||||
123: "BracketLeft",
|
|
||||||
125: "BracketRight",
|
|
||||||
124: "Backslash",
|
|
||||||
95: "Minus",
|
|
||||||
43: "Equal",
|
|
||||||
126: "grave",
|
|
||||||
196: "Adiaeresis",
|
196: "Adiaeresis",
|
||||||
214: "Odiaeresis",
|
214: "Odiaeresis",
|
||||||
220: "Udiaeresis",
|
220: "Udiaeresis",
|
||||||
@@ -126,6 +105,56 @@ const KEY_MAP = {
|
|||||||
161: "exclamdown"
|
161: "exclamdown"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Preserve unshifted symbols from the active layout
|
||||||
|
const SYMBOL_KEYSYM = {
|
||||||
|
33: "exclam",
|
||||||
|
34: "quotedbl",
|
||||||
|
35: "numbersign",
|
||||||
|
36: "dollar",
|
||||||
|
37: "percent",
|
||||||
|
38: "ampersand",
|
||||||
|
40: "parenleft",
|
||||||
|
41: "parenright",
|
||||||
|
42: "asterisk",
|
||||||
|
43: "plus",
|
||||||
|
58: "colon",
|
||||||
|
60: "less",
|
||||||
|
62: "greater",
|
||||||
|
63: "question",
|
||||||
|
64: "at",
|
||||||
|
94: "asciicircum",
|
||||||
|
95: "underscore",
|
||||||
|
123: "braceleft",
|
||||||
|
124: "bar",
|
||||||
|
125: "braceright",
|
||||||
|
126: "asciitilde"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve the existing shifted-US physical-key mapping
|
||||||
|
const SHIFTED_US_FALLBACK = {
|
||||||
|
33: "1",
|
||||||
|
34: "Apostrophe",
|
||||||
|
35: "3",
|
||||||
|
36: "4",
|
||||||
|
37: "5",
|
||||||
|
38: "7",
|
||||||
|
40: "9",
|
||||||
|
41: "0",
|
||||||
|
42: "8",
|
||||||
|
43: "Equal",
|
||||||
|
58: "Semicolon",
|
||||||
|
60: "Comma",
|
||||||
|
62: "Period",
|
||||||
|
63: "Slash",
|
||||||
|
64: "2",
|
||||||
|
94: "6",
|
||||||
|
95: "Minus",
|
||||||
|
123: "BracketLeft",
|
||||||
|
124: "Backslash",
|
||||||
|
125: "BracketRight",
|
||||||
|
126: "grave"
|
||||||
|
};
|
||||||
|
|
||||||
// Numpad (keypad) keys. Qt reuses the same Qt::Key_* values for the numpad and
|
// Numpad (keypad) keys. Qt reuses the same Qt::Key_* values for the numpad and
|
||||||
// the main rows/nav cluster; only Qt.KeypadModifier distinguishes them. niri and
|
// the main rows/nav cluster; only Qt.KeypadModifier distinguishes them. niri and
|
||||||
// the other compositors bind against the xkb KP_* keysym names, so we must emit
|
// the other compositors bind against the xkb KP_* keysym names, so we must emit
|
||||||
@@ -153,13 +182,17 @@ const KP_MAP = {
|
|||||||
46: "KP_Decimal"
|
46: "KP_Decimal"
|
||||||
};
|
};
|
||||||
|
|
||||||
function xkbKeyFromQtKey(qk, isKeypad) {
|
function xkbKeyFromQtKey(qk, isKeypad, hasShift) {
|
||||||
if (isKeypad) {
|
if (isKeypad) {
|
||||||
if (qk >= 48 && qk <= 57)
|
if (qk >= 48 && qk <= 57)
|
||||||
return "KP_" + (qk - 48);
|
return "KP_" + (qk - 48);
|
||||||
if (KP_MAP[qk])
|
if (KP_MAP[qk])
|
||||||
return KP_MAP[qk];
|
return KP_MAP[qk];
|
||||||
}
|
}
|
||||||
|
if (!hasShift && SYMBOL_KEYSYM[qk])
|
||||||
|
return SYMBOL_KEYSYM[qk];
|
||||||
|
if (hasShift && SHIFTED_US_FALLBACK[qk])
|
||||||
|
return SHIFTED_US_FALLBACK[qk];
|
||||||
if (qk >= 65 && qk <= 90)
|
if (qk >= 65 && qk <= 90)
|
||||||
return String.fromCharCode(qk);
|
return String.fromCharCode(qk);
|
||||||
if (qk >= 97 && qk <= 122)
|
if (qk >= 97 && qk <= 122)
|
||||||
@@ -192,23 +225,42 @@ function formatToken(mods, key) {
|
|||||||
return (mods.length ? mods.join("+") + "+" : "") + key;
|
return (mods.length ? mods.join("+") + "+" : "") + key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeKeyCombo(keyCombo) {
|
function canonicalModifier(modifier) {
|
||||||
if (!keyCombo)
|
var normalized = (modifier || "").toLowerCase();
|
||||||
return "";
|
if (normalized === "control")
|
||||||
return keyCombo.toLowerCase().replace(/\bmod\b/g, "super").replace(/\bsuper\b/g, "super");
|
return "ctrl";
|
||||||
|
if (normalized === "win")
|
||||||
|
return "super";
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConflictingBinds(keyCombo, currentAction, allBinds) {
|
function withSymbolicMod(mods, modKey) {
|
||||||
|
var configuredMod = canonicalModifier(modKey);
|
||||||
|
if (!configuredMod)
|
||||||
|
return mods;
|
||||||
|
return mods.map(function (modifier) {
|
||||||
|
return canonicalModifier(modifier) === configuredMod ? "Mod" : modifier;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKeyCombo(keyCombo, modKey) {
|
||||||
|
if (!keyCombo)
|
||||||
|
return "";
|
||||||
|
var configuredMod = canonicalModifier(modKey) || "super";
|
||||||
|
return keyCombo.toLowerCase().replace(/\bmod\b/g, configuredMod).replace(/\bcontrol\b/g, "ctrl").replace(/\bwin\b/g, "super");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConflictingBinds(keyCombo, currentAction, allBinds, modKey) {
|
||||||
if (!keyCombo)
|
if (!keyCombo)
|
||||||
return [];
|
return [];
|
||||||
var conflicts = [];
|
var conflicts = [];
|
||||||
var normalizedKey = normalizeKeyCombo(keyCombo);
|
var normalizedKey = normalizeKeyCombo(keyCombo, modKey);
|
||||||
for (var i = 0; i < allBinds.length; i++) {
|
for (var i = 0; i < allBinds.length; i++) {
|
||||||
var bind = allBinds[i];
|
var bind = allBinds[i];
|
||||||
if (bind.action === currentAction)
|
if (bind.action === currentAction)
|
||||||
continue;
|
continue;
|
||||||
for (var k = 0; k < bind.keys.length; k++) {
|
for (var k = 0; k < bind.keys.length; k++) {
|
||||||
if (normalizeKeyCombo(bind.keys[k].key) === normalizedKey) {
|
if (normalizeKeyCombo(bind.keys[k].key, modKey) === normalizedKey) {
|
||||||
conflicts.push({
|
conflicts.push({
|
||||||
action: bind.action,
|
action: bind.action,
|
||||||
desc: bind.desc || bind.action
|
desc: bind.desc || bind.action
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ Singleton {
|
|||||||
return CompositorService.useHyprlandFocusGrab && keyboardFocus(active, customFocus) === WlrKeyboardFocus.OnDemand;
|
return CompositorService.useHyprlandFocusGrab && keyboardFocus(active, customFocus) === WlrKeyboardFocus.OnDemand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function captureActiveToplevel() {
|
||||||
|
return ToplevelManager.activeToplevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreToplevel(toplevel) {
|
||||||
|
if (toplevel)
|
||||||
|
Qt.callLater(() => toplevel.activate());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
property list<var> barWindows: []
|
property list<var> barWindows: []
|
||||||
|
|
||||||
function registerBarWindow(window) {
|
function registerBarWindow(window) {
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
// English language name -> ISO 639-1 code, for abbreviating xkb layout names.
|
||||||
|
const LANG_CODES = {
|
||||||
|
"afrikaans": "af",
|
||||||
|
"albanian": "sq",
|
||||||
|
"amharic": "am",
|
||||||
|
"arabic": "ar",
|
||||||
|
"armenian": "hy",
|
||||||
|
"azerbaijani": "az",
|
||||||
|
"basque": "eu",
|
||||||
|
"belarusian": "be",
|
||||||
|
"bengali": "bn",
|
||||||
|
"bosnian": "bs",
|
||||||
|
"bulgarian": "bg",
|
||||||
|
"burmese": "my",
|
||||||
|
"catalan": "ca",
|
||||||
|
"chinese": "zh",
|
||||||
|
"croatian": "hr",
|
||||||
|
"czech": "cs",
|
||||||
|
"danish": "da",
|
||||||
|
"dutch": "nl",
|
||||||
|
"english": "en",
|
||||||
|
"esperanto": "eo",
|
||||||
|
"estonian": "et",
|
||||||
|
"filipino": "fil",
|
||||||
|
"finnish": "fi",
|
||||||
|
"french": "fr",
|
||||||
|
"galician": "gl",
|
||||||
|
"georgian": "ka",
|
||||||
|
"german": "de",
|
||||||
|
"greek": "el",
|
||||||
|
"gujarati": "gu",
|
||||||
|
"hausa": "ha",
|
||||||
|
"hebrew": "he",
|
||||||
|
"hindi": "hi",
|
||||||
|
"hungarian": "hu",
|
||||||
|
"icelandic": "is",
|
||||||
|
"igbo": "ig",
|
||||||
|
"indonesian": "id",
|
||||||
|
"irish": "ga",
|
||||||
|
"italian": "it",
|
||||||
|
"japanese": "ja",
|
||||||
|
"javanese": "jv",
|
||||||
|
"kannada": "kn",
|
||||||
|
"kazakh": "kk",
|
||||||
|
"khmer": "km",
|
||||||
|
"korean": "ko",
|
||||||
|
"kurdish": "ku",
|
||||||
|
"kyrgyz": "ky",
|
||||||
|
"lao": "lo",
|
||||||
|
"latvian": "lv",
|
||||||
|
"lithuanian": "lt",
|
||||||
|
"luxembourgish": "lb",
|
||||||
|
"macedonian": "mk",
|
||||||
|
"malay": "ms",
|
||||||
|
"malayalam": "ml",
|
||||||
|
"maltese": "mt",
|
||||||
|
"maori": "mi",
|
||||||
|
"marathi": "mr",
|
||||||
|
"mongolian": "mn",
|
||||||
|
"nepali": "ne",
|
||||||
|
"norwegian": "no",
|
||||||
|
"pashto": "ps",
|
||||||
|
"persian": "fa",
|
||||||
|
"iranian": "fa",
|
||||||
|
"farsi": "fa",
|
||||||
|
"polish": "pl",
|
||||||
|
"portuguese": "pt",
|
||||||
|
"punjabi": "pa",
|
||||||
|
"romanian": "ro",
|
||||||
|
"russian": "ru",
|
||||||
|
"serbian": "sr",
|
||||||
|
"sindhi": "sd",
|
||||||
|
"sinhala": "si",
|
||||||
|
"slovak": "sk",
|
||||||
|
"slovenian": "sl",
|
||||||
|
"somali": "so",
|
||||||
|
"spanish": "es",
|
||||||
|
"swahili": "sw",
|
||||||
|
"swedish": "sv",
|
||||||
|
"tajik": "tg",
|
||||||
|
"tamil": "ta",
|
||||||
|
"tatar": "tt",
|
||||||
|
"telugu": "te",
|
||||||
|
"thai": "th",
|
||||||
|
"tibetan": "bo",
|
||||||
|
"turkish": "tr",
|
||||||
|
"turkmen": "tk",
|
||||||
|
"ukrainian": "uk",
|
||||||
|
"urdu": "ur",
|
||||||
|
"uyghur": "ug",
|
||||||
|
"uzbek": "uz",
|
||||||
|
"vietnamese": "vi",
|
||||||
|
"welsh": "cy",
|
||||||
|
"yiddish": "yi",
|
||||||
|
"yoruba": "yo",
|
||||||
|
"zulu": "zu"
|
||||||
|
};
|
||||||
|
|
||||||
|
function layoutCode(layoutName) {
|
||||||
|
if (!layoutName)
|
||||||
|
return "";
|
||||||
|
const lang = layoutName.split(" ")[0].toLowerCase();
|
||||||
|
const code = LANG_CODES[lang] || lang.substring(0, 2);
|
||||||
|
return code.toUpperCase();
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -16,8 +17,70 @@ Singleton {
|
|||||||
signal popoutOpening
|
signal popoutOpening
|
||||||
signal popoutChanged
|
signal popoutChanged
|
||||||
|
|
||||||
|
property real hoverCursorGlobalX: 0
|
||||||
|
property real hoverCursorGlobalY: 0
|
||||||
|
|
||||||
|
function updateHoverCursor(gx, gy) {
|
||||||
|
hoverCursorGlobalX = gx;
|
||||||
|
hoverCursorGlobalY = gy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cursorOverBar(gx, gy, padding, excludedWindow) {
|
||||||
|
const pad = padding !== undefined ? padding : 16;
|
||||||
|
const bars = KeyboardFocus.barWindows || [];
|
||||||
|
for (let i = 0; i < bars.length; i++) {
|
||||||
|
const w = bars[i];
|
||||||
|
if (!w?.visible || w === excludedWindow)
|
||||||
|
continue;
|
||||||
|
if (typeof w.containsGlobalPoint === "function") {
|
||||||
|
if (w.containsGlobalPoint(gx, gy, pad))
|
||||||
|
return true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const item = w.contentItem;
|
||||||
|
if (!item || typeof item.mapToItem !== "function")
|
||||||
|
continue;
|
||||||
|
const topLeft = item.mapToItem(null, 0, 0);
|
||||||
|
if (!topLeft)
|
||||||
|
continue;
|
||||||
|
if (gx >= topLeft.x - pad && gx < topLeft.x + item.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + item.height + pad)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isPopoutPresented(popout) {
|
||||||
|
if (!popout)
|
||||||
|
return false;
|
||||||
|
try {
|
||||||
|
if (popout.dashVisible !== undefined)
|
||||||
|
return !!popout.dashVisible;
|
||||||
|
if (popout.notificationHistoryVisible !== undefined)
|
||||||
|
return !!popout.notificationHistoryVisible;
|
||||||
|
return !!(popout.shouldBeVisible || popout.isClosing);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _openPopout(popout) {
|
||||||
|
if (popout.dashVisible !== undefined) {
|
||||||
|
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing)
|
||||||
|
popout.dashVisible = false;
|
||||||
|
popout.dashVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (popout.notificationHistoryVisible !== undefined) {
|
||||||
|
popout.notificationHistoryVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popout.open();
|
||||||
|
}
|
||||||
|
|
||||||
function _closePopout(popout) {
|
function _closePopout(popout) {
|
||||||
try {
|
try {
|
||||||
|
if (popout.hoverDismissEnabled !== undefined)
|
||||||
|
popout.hoverDismissEnabled = false;
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case popout.dashVisible !== undefined:
|
case popout.dashVisible !== undefined:
|
||||||
popout.dashVisible = false;
|
popout.dashVisible = false;
|
||||||
@@ -35,6 +98,20 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _dismissPopoutFromHover(popout) {
|
||||||
|
try {
|
||||||
|
if (!popout || popout.hoverDismissEnabled !== true)
|
||||||
|
return;
|
||||||
|
if (typeof popout.closeFromHoverDismiss === "function") {
|
||||||
|
popout.closeFromHoverDismiss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_closePopout(popout);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _isStale(popout) {
|
function _isStale(popout) {
|
||||||
try {
|
try {
|
||||||
if (!popout || !("shouldBeVisible" in popout))
|
if (!popout || !("shouldBeVisible" in popout))
|
||||||
@@ -89,7 +166,39 @@ Singleton {
|
|||||||
continue;
|
continue;
|
||||||
_closePopout(popout);
|
_closePopout(popout);
|
||||||
}
|
}
|
||||||
currentPopoutsByScreen = {};
|
// Keep map entries until each popout's close animation finishes (hidePopout).
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePopoutForScreen(screen) {
|
||||||
|
if (!screen)
|
||||||
|
return;
|
||||||
|
const screenName = screen.name;
|
||||||
|
const popout = currentPopoutsByScreen[screenName];
|
||||||
|
if (!popout || _isStale(popout)) {
|
||||||
|
currentPopoutsByScreen[screenName] = null;
|
||||||
|
currentPopoutTriggers[screenName] = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_closePopout(popout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissHoverPopoutForScreen(screen) {
|
||||||
|
if (!screen)
|
||||||
|
return;
|
||||||
|
const screenName = screen.name;
|
||||||
|
const popout = currentPopoutsByScreen[screenName];
|
||||||
|
if (!popout || _isStale(popout)) {
|
||||||
|
currentPopoutsByScreen[screenName] = null;
|
||||||
|
currentPopoutTriggers[screenName] = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_dismissPopoutFromHover(popout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelHoverDismiss(screen) {
|
||||||
|
const popout = getActivePopout(screen);
|
||||||
|
if (popout?.cancelHoverDismiss)
|
||||||
|
popout.cancelHoverDismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActivePopout(screen) {
|
function getActivePopout(screen) {
|
||||||
@@ -98,23 +207,38 @@ Singleton {
|
|||||||
return currentPopoutsByScreen[screen.name] || null;
|
return currentPopoutsByScreen[screen.name] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if the active popout is pinned for auto-dismissal
|
||||||
|
function isActivePopoutPinned(screen) {
|
||||||
|
const p = getActivePopout(screen);
|
||||||
|
if (!p || !_isPopoutPresented(p))
|
||||||
|
return false;
|
||||||
|
const dismissSuspended = p.effectiveHoverDismissSuspended ?? p.hoverDismissSuspended;
|
||||||
|
return p.hoverDismissEnabled === false || dismissSuspended === true;
|
||||||
|
}
|
||||||
|
|
||||||
function isCurrentPopout(popout, screenName) {
|
function isCurrentPopout(popout, screenName) {
|
||||||
const name = screenName || popout?.screen?.name || "";
|
const name = screenName || popout?.screen?.name || "";
|
||||||
return !!name && currentPopoutsByScreen[name] === popout;
|
return !!name && currentPopoutsByScreen[name] === popout;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestPopout(popout, tabIndex, triggerSource) {
|
function _requestPopout(popout, tabIndex, triggerSource, hoverRequest) {
|
||||||
if (!popout || !popout.screen)
|
if (!popout || !popout.screen)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Clicking a transient popout pins it instead of toggling it closed.
|
||||||
|
const wasTransient = popout.hoverDismissEnabled === true;
|
||||||
|
if (!hoverRequest && popout.hoverDismissEnabled !== undefined)
|
||||||
|
popout.hoverDismissEnabled = false;
|
||||||
|
|
||||||
screenshotActive = false;
|
screenshotActive = false;
|
||||||
const screenName = popout.screen.name;
|
const screenName = popout.screen.name;
|
||||||
const currentPopout = currentPopoutsByScreen[screenName];
|
const currentPopout = currentPopoutsByScreen[screenName];
|
||||||
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
|
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
|
||||||
|
const alreadyPresented = currentPopout === popout && (hoverRequest ? _isPopoutPresented(popout) : popout.shouldBeVisible);
|
||||||
|
|
||||||
const willOpen = !(currentPopout === popout && popout.shouldBeVisible && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
|
const willOpen = !(alreadyPresented && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
|
||||||
if (willOpen) {
|
if (willOpen)
|
||||||
popoutOpening();
|
popoutOpening();
|
||||||
}
|
|
||||||
|
|
||||||
let movedFromOtherScreen = false;
|
let movedFromOtherScreen = false;
|
||||||
for (const otherScreenName in currentPopoutsByScreen) {
|
for (const otherScreenName in currentPopoutsByScreen) {
|
||||||
@@ -145,18 +269,26 @@ Singleton {
|
|||||||
currentPopoutsByScreen[screenName] = null;
|
currentPopoutsByScreen[screenName] = null;
|
||||||
currentPopoutTriggers[screenName] = null;
|
currentPopoutTriggers[screenName] = null;
|
||||||
} else {
|
} else {
|
||||||
|
if (hoverRequest && typeof currentPopout.beginSupersededClose === "function")
|
||||||
|
currentPopout.beginSupersededClose();
|
||||||
_closePopout(currentPopout);
|
_closePopout(currentPopout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
if (alreadyPresented && !movedFromOtherScreen) {
|
||||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
const sameDefinedTrigger = triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId;
|
||||||
|
if (hoverRequest && sameDefinedTrigger)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!hoverRequest && (triggerId === undefined || sameDefinedTrigger)) {
|
||||||
|
if (!wasTransient) {
|
||||||
_closePopout(popout);
|
_closePopout(popout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (popout.updateSurfacePosition)
|
||||||
if (triggerId === undefined) {
|
popout.updateSurfacePosition();
|
||||||
_closePopout(popout);
|
if (triggerId !== undefined)
|
||||||
|
currentPopoutTriggers[screenName] = triggerId;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +298,8 @@ Singleton {
|
|||||||
if (popout.updateSurfacePosition)
|
if (popout.updateSurfacePosition)
|
||||||
popout.updateSurfacePosition();
|
popout.updateSurfacePosition();
|
||||||
currentPopoutTriggers[screenName] = triggerId;
|
currentPopoutTriggers[screenName] = triggerId;
|
||||||
|
if (hoverRequest && popout.hoverDismissEnabled !== undefined)
|
||||||
|
popout.hoverDismissEnabled = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,16 +315,17 @@ Singleton {
|
|||||||
ModalManager.closeAllModalsExcept(null);
|
ModalManager.closeAllModalsExcept(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (movedFromOtherScreen) {
|
if (hoverRequest && popout.hoverDismissEnabled !== undefined)
|
||||||
popout.open();
|
popout.hoverDismissEnabled = true;
|
||||||
} else {
|
|
||||||
if (popout.dashVisible !== undefined) {
|
_openPopout(popout);
|
||||||
popout.dashVisible = true;
|
}
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = true;
|
function requestPopout(popout, tabIndex, triggerSource) {
|
||||||
} else {
|
_requestPopout(popout, tabIndex, triggerSource, false);
|
||||||
popout.open();
|
}
|
||||||
}
|
|
||||||
}
|
function requestHoverPopout(popout, tabIndex, triggerSource) {
|
||||||
|
_requestPopout(popout, tabIndex, triggerSource, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("SettingsData")
|
readonly property var log: Log.scoped("SettingsData")
|
||||||
|
|
||||||
readonly property int settingsConfigVersion: 11
|
readonly property int settingsConfigVersion: 12
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||||
|
|
||||||
@@ -177,10 +177,12 @@ Singleton {
|
|||||||
property int niriLayoutRadiusOverride: -1
|
property int niriLayoutRadiusOverride: -1
|
||||||
property int niriLayoutBorderSize: -1
|
property int niriLayoutBorderSize: -1
|
||||||
property int hyprlandLayoutGapsOverride: -1
|
property int hyprlandLayoutGapsOverride: -1
|
||||||
|
property int hyprlandLayoutGapsOutOverride: -1
|
||||||
property int hyprlandLayoutRadiusOverride: -1
|
property int hyprlandLayoutRadiusOverride: -1
|
||||||
property int hyprlandLayoutBorderSize: -1
|
property int hyprlandLayoutBorderSize: -1
|
||||||
property bool hyprlandResizeOnBorder: false
|
property bool hyprlandResizeOnBorder: false
|
||||||
property int mangoLayoutGapsOverride: -1
|
property int mangoLayoutGapsOverride: -1
|
||||||
|
property int mangoLayoutGapsOutOverride: -1
|
||||||
property int mangoLayoutRadiusOverride: -1
|
property int mangoLayoutRadiusOverride: -1
|
||||||
property int mangoLayoutBorderSize: -1
|
property int mangoLayoutBorderSize: -1
|
||||||
property bool mangoTrackpadNaturalScrolling: true
|
property bool mangoTrackpadNaturalScrolling: true
|
||||||
@@ -262,7 +264,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool frameEnabled: false
|
property bool frameEnabled: false
|
||||||
onFrameEnabledChanged: saveSettings()
|
onFrameEnabledChanged: {
|
||||||
|
saveSettings();
|
||||||
|
if (!_loading)
|
||||||
|
updateFrameCompositorLayout();
|
||||||
|
}
|
||||||
property real frameThickness: 16
|
property real frameThickness: 16
|
||||||
onFrameThicknessChanged: saveSettings()
|
onFrameThicknessChanged: saveSettings()
|
||||||
property int barInsetPaddingShared: -1
|
property int barInsetPaddingShared: -1
|
||||||
@@ -291,9 +297,15 @@ Singleton {
|
|||||||
onFrameLauncherEmergeSideChanged: saveSettings()
|
onFrameLauncherEmergeSideChanged: saveSettings()
|
||||||
property bool frameLauncherArcExtender: false
|
property bool frameLauncherArcExtender: false
|
||||||
onFrameLauncherArcExtenderChanged: saveSettings()
|
onFrameLauncherArcExtenderChanged: saveSettings()
|
||||||
|
property bool frameLauncherEdgeHover: false
|
||||||
|
onFrameLauncherEdgeHoverChanged: saveSettings()
|
||||||
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
||||||
property string frameMode: "connected"
|
property string frameMode: "connected"
|
||||||
onFrameModeChanged: saveSettings()
|
onFrameModeChanged: {
|
||||||
|
saveSettings();
|
||||||
|
if (!_loading && frameEnabled)
|
||||||
|
updateFrameCompositorLayout();
|
||||||
|
}
|
||||||
property var connectedFrameBarStyleBackups: ({})
|
property var connectedFrameBarStyleBackups: ({})
|
||||||
onConnectedFrameBarStyleBackupsChanged: saveSettings()
|
onConnectedFrameBarStyleBackupsChanged: saveSettings()
|
||||||
readonly property bool connectedFrameModeActive: frameEnabled && frameMode === "connected"
|
readonly property bool connectedFrameModeActive: frameEnabled && frameMode === "connected"
|
||||||
@@ -446,9 +458,11 @@ Singleton {
|
|||||||
property string audioScrollMode: "volume"
|
property string audioScrollMode: "volume"
|
||||||
property int audioWheelScrollAmount: 5
|
property int audioWheelScrollAmount: 5
|
||||||
property bool audioDeviceScrollVolumeEnabled: false
|
property bool audioDeviceScrollVolumeEnabled: false
|
||||||
|
property var mediaExcludePlayers: []
|
||||||
property bool clockCompactMode: false
|
property bool clockCompactMode: false
|
||||||
property int focusedWindowSize: 1
|
property int focusedWindowSize: 1
|
||||||
property bool focusedWindowCompactMode: false
|
property bool focusedWindowCompactMode: false
|
||||||
|
property bool focusedWindowShowIcon: false
|
||||||
property bool runningAppsCompactMode: true
|
property bool runningAppsCompactMode: true
|
||||||
property int barMaxVisibleApps: 0
|
property int barMaxVisibleApps: 0
|
||||||
property int barMaxVisibleRunningApps: 0
|
property int barMaxVisibleRunningApps: 0
|
||||||
@@ -483,6 +497,8 @@ Singleton {
|
|||||||
property string greeterLockDateFormat: ""
|
property string greeterLockDateFormat: ""
|
||||||
property string greeterFontFamily: ""
|
property string greeterFontFamily: ""
|
||||||
property string greeterWallpaperFillMode: ""
|
property string greeterWallpaperFillMode: ""
|
||||||
|
property bool greeterSyncPending: false
|
||||||
|
property var greeterSyncBaseline: ({})
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -737,12 +753,16 @@ Singleton {
|
|||||||
property bool batteryNotifyCritical: true
|
property bool batteryNotifyCritical: true
|
||||||
property int batteryLowThreshold: 20
|
property int batteryLowThreshold: 20
|
||||||
property bool batteryNotifyLow: false
|
property bool batteryNotifyLow: false
|
||||||
property int batteryNotificationType: 0
|
property int batteryChargeLimitNotificationType: 0
|
||||||
|
property int batteryLowNotificationType: 0
|
||||||
|
property int batteryCriticalNotificationType: 1
|
||||||
property bool batteryAutoPowerSaver: false
|
property bool batteryAutoPowerSaver: false
|
||||||
property bool showBatteryPercent: true
|
property bool showBatteryPercent: true
|
||||||
property bool showBatteryPercentOnlyOnBattery: false
|
property bool showBatteryPercentOnlyOnBattery: false
|
||||||
property bool showBatteryTime: false
|
property bool showBatteryTime: false
|
||||||
property bool showBatteryTimeOnlyOnBattery: false
|
property bool showBatteryTimeOnlyOnBattery: false
|
||||||
|
property bool batteryPillStyle: false
|
||||||
|
property bool batteryPillPercentSign: false
|
||||||
property bool lockBeforeSuspend: false
|
property bool lockBeforeSuspend: false
|
||||||
property bool loginctlLockIntegration: true
|
property bool loginctlLockIntegration: true
|
||||||
property bool fadeToLockEnabled: true
|
property bool fadeToLockEnabled: true
|
||||||
@@ -874,12 +894,14 @@ Singleton {
|
|||||||
readonly property bool greeterU2fReady: Processes.greeterU2fReady
|
readonly property bool greeterU2fReady: Processes.greeterU2fReady
|
||||||
readonly property string greeterU2fReason: Processes.greeterU2fReason
|
readonly property string greeterU2fReason: Processes.greeterU2fReason
|
||||||
readonly property string greeterU2fSource: Processes.greeterU2fSource
|
readonly property string greeterU2fSource: Processes.greeterU2fSource
|
||||||
property string lockScreenActiveMonitor: "all"
|
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
property bool lockScreenVideoEnabled: false
|
property bool lockScreenVideoEnabled: false
|
||||||
property string lockScreenVideoPath: ""
|
property string lockScreenVideoPath: ""
|
||||||
property bool lockScreenVideoCycling: false
|
property bool lockScreenVideoCycling: false
|
||||||
|
property string lockScreenWallpaperPath: ""
|
||||||
|
property string lockScreenWallpaperFillMode: ""
|
||||||
|
property string lockScreenFontFamily: ""
|
||||||
property bool hideBrightnessSlider: false
|
property bool hideBrightnessSlider: false
|
||||||
|
|
||||||
property int notificationTimeoutLow: 5000
|
property int notificationTimeoutLow: 5000
|
||||||
@@ -999,10 +1021,23 @@ Singleton {
|
|||||||
"shadowOpacity": 60,
|
"shadowOpacity": 60,
|
||||||
"shadowColorMode": "default",
|
"shadowColorMode": "default",
|
||||||
"shadowCustomColor": "#000000",
|
"shadowCustomColor": "#000000",
|
||||||
"clickThrough": false
|
"clickThrough": false,
|
||||||
|
"hoverPopouts": false,
|
||||||
|
"hoverPopoutDelay": 150
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Standalone bar xray is unsafe when windows can render beneath its surface
|
||||||
|
function _standaloneBarXrayAvailable(configs) {
|
||||||
|
const list = configs || [];
|
||||||
|
const activeBars = list.filter(c => c && c.enabled && (c.visible ?? true));
|
||||||
|
const gapsOverride = (typeof CompositorService !== "undefined" && CompositorService.isHyprland) ? hyprlandLayoutGapsOverride : niriLayoutGapsOverride;
|
||||||
|
const layoutGaps = gapsOverride >= 0 ? gapsOverride : Math.max(4, (list[0]?.spacing ?? 4));
|
||||||
|
return activeBars.every(c => !c.autoHide && !(c.useOverlayLayer ?? false) && (c.spacing ?? 4) + (c.bottomGap ?? 0) + layoutGaps >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool standaloneBarXrayAvailable: _standaloneBarXrayAvailable(barConfigs)
|
||||||
|
|
||||||
property bool desktopClockEnabled: false
|
property bool desktopClockEnabled: false
|
||||||
property string desktopClockStyle: "analog"
|
property string desktopClockStyle: "analog"
|
||||||
property real desktopClockTransparency: 0.8
|
property real desktopClockTransparency: 0.8
|
||||||
@@ -1318,6 +1353,35 @@ Singleton {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveDesktopWidgetInstanceToGroup(instanceId, groupId, newIndexInGroup) {
|
||||||
|
const instances = JSON.parse(JSON.stringify(desktopWidgetInstances || []));
|
||||||
|
const groups = desktopWidgetGroups || [];
|
||||||
|
const idx = instances.findIndex(inst => inst.id === instanceId);
|
||||||
|
if (idx === -1)
|
||||||
|
return false;
|
||||||
|
const [item] = instances.splice(idx, 1);
|
||||||
|
item.group = groupId || null;
|
||||||
|
const groupMatches = inst => {
|
||||||
|
if (!groupId)
|
||||||
|
return !inst.group || !groups.some(g => g.id === inst.group);
|
||||||
|
return inst.group === groupId;
|
||||||
|
};
|
||||||
|
const groupInstances = instances.filter(groupMatches);
|
||||||
|
const clamped = Math.max(0, Math.min(newIndexInGroup, groupInstances.length));
|
||||||
|
let targetGlobalIdx;
|
||||||
|
if (clamped >= groupInstances.length) {
|
||||||
|
const last = groupInstances[groupInstances.length - 1];
|
||||||
|
targetGlobalIdx = last ? instances.findIndex(inst => inst.id === last.id) + 1 : instances.length;
|
||||||
|
} else {
|
||||||
|
const targetInstance = groupInstances[clamped];
|
||||||
|
targetGlobalIdx = instances.findIndex(inst => inst.id === targetInstance.id);
|
||||||
|
}
|
||||||
|
instances.splice(targetGlobalIdx, 0, item);
|
||||||
|
desktopWidgetInstances = instances;
|
||||||
|
saveSettings();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function createDesktopWidgetGroup(name) {
|
function createDesktopWidgetGroup(name) {
|
||||||
const id = "dwg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
|
const id = "dwg_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
|
||||||
const group = {
|
const group = {
|
||||||
@@ -1418,6 +1482,17 @@ Singleton {
|
|||||||
MangoService.generateLayoutConfig();
|
MangoService.generateLayoutConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateFrameCompositorLayout() {
|
||||||
|
// Generate before begin() so compositor readiness is already pending at transitionRequested
|
||||||
|
if (typeof CompositorService !== "undefined") {
|
||||||
|
if (CompositorService.isNiri && typeof NiriService !== "undefined")
|
||||||
|
NiriService.generateNiriLayoutConfig(true);
|
||||||
|
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
|
||||||
|
HyprlandService.generateLayoutConfig(true);
|
||||||
|
}
|
||||||
|
FrameTransitionState.begin();
|
||||||
|
}
|
||||||
|
|
||||||
function resolveIconTheme() {
|
function resolveIconTheme() {
|
||||||
if (iconThemePerMode && typeof SessionData !== "undefined" && SessionData.isLightMode)
|
if (iconThemePerMode && typeof SessionData !== "undefined" && SessionData.isLightMode)
|
||||||
return iconThemeLight;
|
return iconThemeLight;
|
||||||
@@ -1613,6 +1688,32 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markGreeterSyncPending(who, key, oldValue) {
|
||||||
|
if (isGreeterMode)
|
||||||
|
return;
|
||||||
|
if (!(key in greeterSyncBaseline)) {
|
||||||
|
var baseline = greeterSyncBaseline;
|
||||||
|
baseline[key] = oldValue;
|
||||||
|
greeterSyncBaseline = baseline;
|
||||||
|
}
|
||||||
|
greeterSyncPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGreeterSyncPending() {
|
||||||
|
greeterSyncBaseline = {};
|
||||||
|
greeterSyncPending = false;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function revertGreeterSyncPending() {
|
||||||
|
for (var key in greeterSyncBaseline) {
|
||||||
|
root[key] = greeterSyncBaseline[key];
|
||||||
|
}
|
||||||
|
greeterSyncBaseline = {};
|
||||||
|
greeterSyncPending = false;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({
|
||||||
"applyStoredTheme": applyStoredTheme,
|
"applyStoredTheme": applyStoredTheme,
|
||||||
"regenSystemThemes": regenSystemThemes,
|
"regenSystemThemes": regenSystemThemes,
|
||||||
@@ -1621,7 +1722,8 @@ Singleton {
|
|||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor,
|
"updateCompositorCursor": updateCompositorCursor,
|
||||||
"scheduleAuthApply": scheduleAuthApply,
|
"scheduleAuthApply": scheduleAuthApply,
|
||||||
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync
|
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync,
|
||||||
|
"markGreeterSyncPending": markGreeterSyncPending
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
@@ -1647,6 +1749,18 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (obj?.lockScreenActiveMonitor !== undefined) {
|
||||||
|
var oldVal = obj.lockScreenActiveMonitor;
|
||||||
|
if (oldVal && oldVal !== "all") {
|
||||||
|
if (!obj.screenPreferences)
|
||||||
|
obj.screenPreferences = {};
|
||||||
|
if (obj.screenPreferences.lockScreen === undefined) {
|
||||||
|
obj.screenPreferences.lockScreen = [oldVal];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete obj.lockScreenActiveMonitor;
|
||||||
|
}
|
||||||
|
|
||||||
Store.parse(root, obj);
|
Store.parse(root, obj);
|
||||||
|
|
||||||
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
|
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
|
||||||
@@ -2365,13 +2479,17 @@ Singleton {
|
|||||||
if (index === -1)
|
if (index === -1)
|
||||||
return;
|
return;
|
||||||
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
|
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
|
||||||
|
const barXrayTargetWasAvailable = _standaloneBarXrayAvailable(configs);
|
||||||
if (updates.autoHide === false || updates.visible === false)
|
if (updates.autoHide === false || updates.visible === false)
|
||||||
setBarIpcReveal(barId, false);
|
setBarIpcReveal(barId, false);
|
||||||
|
|
||||||
Object.assign(configs[index], updates);
|
Object.assign(configs[index], updates);
|
||||||
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
const sanitizedConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
||||||
|
barConfigs = sanitizedConfigs;
|
||||||
updateBarConfigs();
|
updateBarConfigs();
|
||||||
|
|
||||||
|
if (!frameEnabled && _standaloneBarXrayAvailable(sanitizedConfigs) !== barXrayTargetWasAvailable)
|
||||||
|
updateCompositorLayout();
|
||||||
if (positionChanged) {
|
if (positionChanged) {
|
||||||
NotificationService.dismissAllPopups();
|
NotificationService.dismissAllPopups();
|
||||||
}
|
}
|
||||||
@@ -2436,6 +2554,46 @@ Singleton {
|
|||||||
return barConfigs.filter(cfg => cfg.enabled);
|
return barConfigs.filter(cfg => cfg.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _sideToPosition(side) {
|
||||||
|
switch (side) {
|
||||||
|
case "top":
|
||||||
|
return SettingsData.Position.Top;
|
||||||
|
case "bottom":
|
||||||
|
return SettingsData.Position.Bottom;
|
||||||
|
case "left":
|
||||||
|
return SettingsData.Position.Left;
|
||||||
|
case "right":
|
||||||
|
return SettingsData.Position.Right;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a bar occupies the specified screen edge
|
||||||
|
function barOccupiesSide(screen, side) {
|
||||||
|
if (!screen)
|
||||||
|
return false;
|
||||||
|
const sidePos = _sideToPosition(side);
|
||||||
|
if (sidePos < 0)
|
||||||
|
return false;
|
||||||
|
const bars = getEnabledBarConfigs();
|
||||||
|
for (var i = 0; i < bars.length; i++) {
|
||||||
|
const bc = bars[i];
|
||||||
|
if (bc.position !== sidePos)
|
||||||
|
continue;
|
||||||
|
const prefs = bc.screenPreferences || ["all"];
|
||||||
|
if (prefs.includes("all") || isScreenInPreferences(screen, prefs))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the dock occupies the specified screen edge.
|
||||||
|
function dockOccupiesSide(side) {
|
||||||
|
if (!showDock)
|
||||||
|
return false;
|
||||||
|
return dockPosition === _sideToPosition(side);
|
||||||
|
}
|
||||||
|
|
||||||
function getScreensSortedByPosition() {
|
function getScreensSortedByPosition() {
|
||||||
const screens = [];
|
const screens = [];
|
||||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||||
@@ -3018,6 +3176,32 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addMediaExcludePlayer(identity) {
|
||||||
|
if (identity === undefined || identity === null)
|
||||||
|
return;
|
||||||
|
var normalizedIdentity = identity.toString().trim().toLowerCase();
|
||||||
|
if (!normalizedIdentity)
|
||||||
|
return;
|
||||||
|
var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : [];
|
||||||
|
var normalizedList = list.map(function (id) {
|
||||||
|
return id ? id.toString().trim().toLowerCase() : "";
|
||||||
|
});
|
||||||
|
if (normalizedList.indexOf(normalizedIdentity) >= 0)
|
||||||
|
return;
|
||||||
|
list.push(normalizedIdentity);
|
||||||
|
mediaExcludePlayers = list;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMediaExcludePlayer(index) {
|
||||||
|
var list = mediaExcludePlayers ? mediaExcludePlayers.slice() : [];
|
||||||
|
if (index < 0 || index >= list.length)
|
||||||
|
return;
|
||||||
|
list.splice(index, 1);
|
||||||
|
mediaExcludePlayers = list;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
property bool _pendingExpandNotificationRules: false
|
property bool _pendingExpandNotificationRules: false
|
||||||
property int _pendingNotificationRuleIndex: -1
|
property int _pendingNotificationRuleIndex: -1
|
||||||
|
|
||||||
@@ -3438,6 +3622,9 @@ Singleton {
|
|||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode)
|
if (isGreeterMode)
|
||||||
return;
|
return;
|
||||||
|
const wasLoaded = _hasLoaded;
|
||||||
|
const prevFrameEnabled = frameEnabled;
|
||||||
|
const prevFrameMode = frameMode;
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_hasUnsavedChanges = false;
|
_hasUnsavedChanges = false;
|
||||||
try {
|
try {
|
||||||
@@ -3472,6 +3659,9 @@ Singleton {
|
|||||||
} finally {
|
} finally {
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
// External edits reload under _loading, which skips the per-property transition triggers
|
||||||
|
if (wasLoaded && !_parseError && (frameEnabled !== prevFrameEnabled || (frameEnabled && frameMode !== prevFrameMode)))
|
||||||
|
updateFrameCompositorLayout();
|
||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const StockThemes = {
|
|||||||
background: "#101418",
|
background: "#101418",
|
||||||
backgroundText: "#e0e2e8",
|
backgroundText: "#e0e2e8",
|
||||||
outline: "#8c9199",
|
outline: "#8c9199",
|
||||||
|
surfaceContainerLowest: "#0c1014",
|
||||||
|
surfaceContainerLow: "#191c20",
|
||||||
surfaceContainer: "#1d2024",
|
surfaceContainer: "#1d2024",
|
||||||
surfaceContainerHigh: "#272a2f",
|
surfaceContainerHigh: "#272a2f",
|
||||||
surfaceContainerHighest: "#32353a",
|
surfaceContainerHighest: "#32353a",
|
||||||
@@ -35,6 +37,8 @@ const StockThemes = {
|
|||||||
background: "#141218",
|
background: "#141218",
|
||||||
backgroundText: "#e6e0e9",
|
backgroundText: "#e6e0e9",
|
||||||
outline: "#948f99",
|
outline: "#948f99",
|
||||||
|
surfaceContainerLowest: "#100e14",
|
||||||
|
surfaceContainerLow: "#1d1b20",
|
||||||
surfaceContainer: "#211f24",
|
surfaceContainer: "#211f24",
|
||||||
surfaceContainerHigh: "#2b292f",
|
surfaceContainerHigh: "#2b292f",
|
||||||
surfaceContainerHighest: "#36343a",
|
surfaceContainerHighest: "#36343a",
|
||||||
@@ -53,6 +57,8 @@ const StockThemes = {
|
|||||||
background: "#10140f",
|
background: "#10140f",
|
||||||
backgroundText: "#e0e4db",
|
backgroundText: "#e0e4db",
|
||||||
outline: "#8c9388",
|
outline: "#8c9388",
|
||||||
|
surfaceContainerLowest: "#0c100b",
|
||||||
|
surfaceContainerLow: "#191d17",
|
||||||
surfaceContainer: "#1d211b",
|
surfaceContainer: "#1d211b",
|
||||||
surfaceContainerHigh: "#272b25",
|
surfaceContainerHigh: "#272b25",
|
||||||
surfaceContainerHighest: "#323630",
|
surfaceContainerHighest: "#323630",
|
||||||
@@ -71,6 +77,8 @@ const StockThemes = {
|
|||||||
background: "#1a120e",
|
background: "#1a120e",
|
||||||
backgroundText: "#f0dfd8",
|
backgroundText: "#f0dfd8",
|
||||||
outline: "#a08d85",
|
outline: "#a08d85",
|
||||||
|
surfaceContainerLowest: "#160e0a",
|
||||||
|
surfaceContainerLow: "#231a16",
|
||||||
surfaceContainer: "#271e1a",
|
surfaceContainer: "#271e1a",
|
||||||
surfaceContainerHigh: "#322824",
|
surfaceContainerHigh: "#322824",
|
||||||
surfaceContainerHighest: "#3d332e",
|
surfaceContainerHighest: "#3d332e",
|
||||||
@@ -89,6 +97,8 @@ const StockThemes = {
|
|||||||
background: "#1a1110",
|
background: "#1a1110",
|
||||||
backgroundText: "#f1dedc",
|
backgroundText: "#f1dedc",
|
||||||
outline: "#a08c89",
|
outline: "#a08c89",
|
||||||
|
surfaceContainerLowest: "#160d0c",
|
||||||
|
surfaceContainerLow: "#231918",
|
||||||
surfaceContainer: "#271d1c",
|
surfaceContainer: "#271d1c",
|
||||||
surfaceContainerHigh: "#322826",
|
surfaceContainerHigh: "#322826",
|
||||||
surfaceContainerHighest: "#3d3231",
|
surfaceContainerHighest: "#3d3231",
|
||||||
@@ -107,6 +117,8 @@ const StockThemes = {
|
|||||||
background: "#0e1416",
|
background: "#0e1416",
|
||||||
backgroundText: "#dee3e5",
|
backgroundText: "#dee3e5",
|
||||||
outline: "#899295",
|
outline: "#899295",
|
||||||
|
surfaceContainerLowest: "#0a1012",
|
||||||
|
surfaceContainerLow: "#171d1e",
|
||||||
surfaceContainer: "#1b2122",
|
surfaceContainer: "#1b2122",
|
||||||
surfaceContainerHigh: "#252b2c",
|
surfaceContainerHigh: "#252b2c",
|
||||||
surfaceContainerHighest: "#303637",
|
surfaceContainerHighest: "#303637",
|
||||||
@@ -125,6 +137,8 @@ const StockThemes = {
|
|||||||
background: "#191112",
|
background: "#191112",
|
||||||
backgroundText: "#f0dee0",
|
backgroundText: "#f0dee0",
|
||||||
outline: "#9f8c8e",
|
outline: "#9f8c8e",
|
||||||
|
surfaceContainerLowest: "#150d0e",
|
||||||
|
surfaceContainerLow: "#22191a",
|
||||||
surfaceContainer: "#261d1e",
|
surfaceContainer: "#261d1e",
|
||||||
surfaceContainerHigh: "#312829",
|
surfaceContainerHigh: "#312829",
|
||||||
surfaceContainerHighest: "#3c3233",
|
surfaceContainerHighest: "#3c3233",
|
||||||
@@ -143,6 +157,8 @@ const StockThemes = {
|
|||||||
background: "#17130b",
|
background: "#17130b",
|
||||||
backgroundText: "#ebe1d4",
|
backgroundText: "#ebe1d4",
|
||||||
outline: "#998f80",
|
outline: "#998f80",
|
||||||
|
surfaceContainerLowest: "#130f07",
|
||||||
|
surfaceContainerLow: "#1f1b13",
|
||||||
surfaceContainer: "#231f17",
|
surfaceContainer: "#231f17",
|
||||||
surfaceContainerHigh: "#2e2921",
|
surfaceContainerHigh: "#2e2921",
|
||||||
surfaceContainerHighest: "#39342b",
|
surfaceContainerHighest: "#39342b",
|
||||||
@@ -161,6 +177,8 @@ const StockThemes = {
|
|||||||
background: "#1a1110",
|
background: "#1a1110",
|
||||||
backgroundText: "#f1dedc",
|
backgroundText: "#f1dedc",
|
||||||
outline: "#a08c8a",
|
outline: "#a08c8a",
|
||||||
|
surfaceContainerLowest: "#160d0c",
|
||||||
|
surfaceContainerLow: "#231918",
|
||||||
surfaceContainer: "#271d1c",
|
surfaceContainer: "#271d1c",
|
||||||
surfaceContainerHigh: "#322826",
|
surfaceContainerHigh: "#322826",
|
||||||
surfaceContainerHighest: "#3d3231",
|
surfaceContainerHighest: "#3d3231",
|
||||||
@@ -179,6 +197,8 @@ const StockThemes = {
|
|||||||
background: "#131315",
|
background: "#131315",
|
||||||
backgroundText: "#e4e2e3",
|
backgroundText: "#e4e2e3",
|
||||||
outline: "#929092",
|
outline: "#929092",
|
||||||
|
surfaceContainerLowest: "#252525",
|
||||||
|
surfaceContainerLow: "#303030",
|
||||||
surfaceContainer: "#353535",
|
surfaceContainer: "#353535",
|
||||||
surfaceContainerHigh: "#424242",
|
surfaceContainerHigh: "#424242",
|
||||||
surfaceContainerHighest: "#505050",
|
surfaceContainerHighest: "#505050",
|
||||||
@@ -203,6 +223,8 @@ const StockThemes = {
|
|||||||
background: "#f7f9ff",
|
background: "#f7f9ff",
|
||||||
backgroundText: "#181c20",
|
backgroundText: "#181c20",
|
||||||
outline: "#72777f",
|
outline: "#72777f",
|
||||||
|
surfaceContainerLowest: "#f9fbff",
|
||||||
|
surfaceContainerLow: "#eef0f6",
|
||||||
surfaceContainer: "#eceef4",
|
surfaceContainer: "#eceef4",
|
||||||
surfaceContainerHigh: "#e6e8ee",
|
surfaceContainerHigh: "#e6e8ee",
|
||||||
surfaceContainerHighest: "#e0e2e8",
|
surfaceContainerHighest: "#e0e2e8",
|
||||||
@@ -221,6 +243,8 @@ const StockThemes = {
|
|||||||
background: "#fef7ff",
|
background: "#fef7ff",
|
||||||
backgroundText: "#1d1b20",
|
backgroundText: "#1d1b20",
|
||||||
outline: "#7a757f",
|
outline: "#7a757f",
|
||||||
|
surfaceContainerLowest: "#fff9ff",
|
||||||
|
surfaceContainerLow: "#f4eef6",
|
||||||
surfaceContainer: "#f2ecf4",
|
surfaceContainer: "#f2ecf4",
|
||||||
surfaceContainerHigh: "#ece6ee",
|
surfaceContainerHigh: "#ece6ee",
|
||||||
surfaceContainerHighest: "#e6e0e9",
|
surfaceContainerHighest: "#e6e0e9",
|
||||||
@@ -239,6 +263,8 @@ const StockThemes = {
|
|||||||
background: "#f7fbf1",
|
background: "#f7fbf1",
|
||||||
backgroundText: "#191d17",
|
backgroundText: "#191d17",
|
||||||
outline: "#72796f",
|
outline: "#72796f",
|
||||||
|
surfaceContainerLowest: "#f9fdf3",
|
||||||
|
surfaceContainerLow: "#eef1e8",
|
||||||
surfaceContainer: "#ecefe6",
|
surfaceContainer: "#ecefe6",
|
||||||
surfaceContainerHigh: "#e6e9e0",
|
surfaceContainerHigh: "#e6e9e0",
|
||||||
surfaceContainerHighest: "#e0e4db",
|
surfaceContainerHighest: "#e0e4db",
|
||||||
@@ -257,6 +283,8 @@ const StockThemes = {
|
|||||||
background: "#fff8f6",
|
background: "#fff8f6",
|
||||||
backgroundText: "#221a16",
|
backgroundText: "#221a16",
|
||||||
outline: "#85736c",
|
outline: "#85736c",
|
||||||
|
surfaceContainerLowest: "#fffaf8",
|
||||||
|
surfaceContainerLow: "#feece5",
|
||||||
surfaceContainer: "#fceae3",
|
surfaceContainer: "#fceae3",
|
||||||
surfaceContainerHigh: "#f6e5de",
|
surfaceContainerHigh: "#f6e5de",
|
||||||
surfaceContainerHighest: "#f0dfd8",
|
surfaceContainerHighest: "#f0dfd8",
|
||||||
@@ -275,6 +303,8 @@ const StockThemes = {
|
|||||||
background: "#fff8f7",
|
background: "#fff8f7",
|
||||||
backgroundText: "#231918",
|
backgroundText: "#231918",
|
||||||
outline: "#857370",
|
outline: "#857370",
|
||||||
|
surfaceContainerLowest: "#fffaf9",
|
||||||
|
surfaceContainerLow: "#feece9",
|
||||||
surfaceContainer: "#fceae7",
|
surfaceContainer: "#fceae7",
|
||||||
surfaceContainerHigh: "#f7e4e1",
|
surfaceContainerHigh: "#f7e4e1",
|
||||||
surfaceContainerHighest: "#f1dedc",
|
surfaceContainerHighest: "#f1dedc",
|
||||||
@@ -293,6 +323,8 @@ const StockThemes = {
|
|||||||
background: "#f5fafc",
|
background: "#f5fafc",
|
||||||
backgroundText: "#171d1e",
|
backgroundText: "#171d1e",
|
||||||
outline: "#6f797b",
|
outline: "#6f797b",
|
||||||
|
surfaceContainerLowest: "#f7fcfe",
|
||||||
|
surfaceContainerLow: "#ebf1f2",
|
||||||
surfaceContainer: "#e9eff0",
|
surfaceContainer: "#e9eff0",
|
||||||
surfaceContainerHigh: "#e3e9eb",
|
surfaceContainerHigh: "#e3e9eb",
|
||||||
surfaceContainerHighest: "#dee3e5",
|
surfaceContainerHighest: "#dee3e5",
|
||||||
@@ -311,6 +343,8 @@ const StockThemes = {
|
|||||||
background: "#fff8f7",
|
background: "#fff8f7",
|
||||||
backgroundText: "#22191a",
|
backgroundText: "#22191a",
|
||||||
outline: "#847375",
|
outline: "#847375",
|
||||||
|
surfaceContainerLowest: "#fffaf9",
|
||||||
|
surfaceContainerLow: "#fdeced",
|
||||||
surfaceContainer: "#fbeaeb",
|
surfaceContainer: "#fbeaeb",
|
||||||
surfaceContainerHigh: "#f5e4e5",
|
surfaceContainerHigh: "#f5e4e5",
|
||||||
surfaceContainerHighest: "#f0dee0",
|
surfaceContainerHighest: "#f0dee0",
|
||||||
@@ -329,6 +363,8 @@ const StockThemes = {
|
|||||||
background: "#fff8f2",
|
background: "#fff8f2",
|
||||||
backgroundText: "#1f1b13",
|
backgroundText: "#1f1b13",
|
||||||
outline: "#7f7667",
|
outline: "#7f7667",
|
||||||
|
surfaceContainerLowest: "#fffaf4",
|
||||||
|
surfaceContainerLow: "#f8eee1",
|
||||||
surfaceContainer: "#f6ecdf",
|
surfaceContainer: "#f6ecdf",
|
||||||
surfaceContainerHigh: "#f1e7d9",
|
surfaceContainerHigh: "#f1e7d9",
|
||||||
surfaceContainerHighest: "#ebe1d4",
|
surfaceContainerHighest: "#ebe1d4",
|
||||||
@@ -347,6 +383,8 @@ const StockThemes = {
|
|||||||
background: "#fff8f7",
|
background: "#fff8f7",
|
||||||
backgroundText: "#231918",
|
backgroundText: "#231918",
|
||||||
outline: "#857371",
|
outline: "#857371",
|
||||||
|
surfaceContainerLowest: "#fffaf9",
|
||||||
|
surfaceContainerLow: "#feece9",
|
||||||
surfaceContainer: "#fceae7",
|
surfaceContainer: "#fceae7",
|
||||||
surfaceContainerHigh: "#f6e4e2",
|
surfaceContainerHigh: "#f6e4e2",
|
||||||
surfaceContainerHighest: "#f1dedc",
|
surfaceContainerHighest: "#f1dedc",
|
||||||
@@ -365,6 +403,8 @@ const StockThemes = {
|
|||||||
background: "#ffffff",
|
background: "#ffffff",
|
||||||
backgroundText: "#1a1a1a",
|
backgroundText: "#1a1a1a",
|
||||||
outline: "#757577",
|
outline: "#757577",
|
||||||
|
surfaceContainerLowest: "#fafafb",
|
||||||
|
surfaceContainerLow: "#ededef",
|
||||||
surfaceContainer: "#e8e8ea",
|
surfaceContainer: "#e8e8ea",
|
||||||
surfaceContainerHigh: "#dcdcde",
|
surfaceContainerHigh: "#dcdcde",
|
||||||
surfaceContainerHighest: "#d0d0d2",
|
surfaceContainerHighest: "#d0d0d2",
|
||||||
|
|||||||
+78
-53
@@ -142,6 +142,8 @@ Singleton {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Quickshell.execDetached(["mkdir", "-p", stateDir]);
|
Quickshell.execDetached(["mkdir", "-p", stateDir]);
|
||||||
|
if (typeof SessionData === "undefined" || !SessionData.isGreeterMode)
|
||||||
|
Quickshell.execDetached([shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]);
|
||||||
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
|
Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => {
|
||||||
matugenAvailable = (code === 0) && !envDisableMatugen;
|
matugenAvailable = (code === 0) && !envDisableMatugen;
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
||||||
@@ -463,6 +465,8 @@ Singleton {
|
|||||||
"background": getMatugenColor("background", "#1a1c1e"),
|
"background": getMatugenColor("background", "#1a1c1e"),
|
||||||
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
"backgroundText": getMatugenColor("on_background", "#e3e8ef"),
|
||||||
"outline": getMatugenColor("outline", "#8e918f"),
|
"outline": getMatugenColor("outline", "#8e918f"),
|
||||||
|
"surfaceContainerLowest": getMatugenColor("surface_container_lowest", "#0e1013"),
|
||||||
|
"surfaceContainerLow": getMatugenColor("surface_container_low", "#181a1d"),
|
||||||
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
"surfaceContainer": getMatugenColor("surface_container", "#1e2023"),
|
||||||
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
"surfaceContainerHigh": getMatugenColor("surface_container_high", "#292b2f"),
|
||||||
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
|
"surfaceContainerHighest": getMatugenColor("surface_container_highest", "#343740"),
|
||||||
@@ -535,7 +539,9 @@ Singleton {
|
|||||||
property color background: currentThemeData.background
|
property color background: currentThemeData.background
|
||||||
property color backgroundText: currentThemeData.backgroundText
|
property color backgroundText: currentThemeData.backgroundText
|
||||||
property color outline: currentThemeData.outline
|
property color outline: currentThemeData.outline
|
||||||
property color outlineVariant: currentThemeData.outlineVariant || Qt.rgba(outline.r, outline.g, outline.b, 0.6)
|
property color outlineVariant: currentThemeData.outlineVariant || withAlpha(outline, 0.6)
|
||||||
|
property color surfaceContainerLowest: currentThemeData.surfaceContainerLowest || blend(surfaceContainer, surface, 1.2)
|
||||||
|
property color surfaceContainerLow: currentThemeData.surfaceContainerLow || blend(surface, surfaceContainer, 0.667)
|
||||||
property color surfaceContainer: currentThemeData.surfaceContainer
|
property color surfaceContainer: currentThemeData.surfaceContainer
|
||||||
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
|
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
|
||||||
property color surfaceContainerHighest: currentThemeData.surfaceContainerHighest || surfaceContainerHigh
|
property color surfaceContainerHighest: currentThemeData.surfaceContainerHighest || surfaceContainerHigh
|
||||||
@@ -546,9 +552,9 @@ Singleton {
|
|||||||
property color onSurface: surfaceText
|
property color onSurface: surfaceText
|
||||||
property color onSurfaceVariant: surfaceVariantText
|
property color onSurfaceVariant: surfaceVariantText
|
||||||
property color onPrimary: primaryText
|
property color onPrimary: primaryText
|
||||||
property color onSurface_12: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.12)
|
property color onSurface_12: withAlpha(onSurface, 0.12)
|
||||||
property color onSurface_38: Qt.rgba(onSurface.r, onSurface.g, onSurface.b, 0.38)
|
property color onSurface_38: withAlpha(onSurface, 0.38)
|
||||||
property color onSurfaceVariant_30: Qt.rgba(onSurfaceVariant.r, onSurfaceVariant.g, onSurfaceVariant.b, 0.30)
|
property color onSurfaceVariant_30: withAlpha(onSurfaceVariant, 0.30)
|
||||||
|
|
||||||
property color error: currentThemeData.error || "#F2B8B5"
|
property color error: currentThemeData.error || "#F2B8B5"
|
||||||
property color warning: currentThemeData.warning || "#FF9800"
|
property color warning: currentThemeData.warning || "#FF9800"
|
||||||
@@ -557,32 +563,33 @@ Singleton {
|
|||||||
property color tempDanger: "#ff5555"
|
property color tempDanger: "#ff5555"
|
||||||
property color success: currentThemeData.success || "#4CAF50"
|
property color success: currentThemeData.success || "#4CAF50"
|
||||||
|
|
||||||
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
|
property color primaryHover: withAlpha(primary, 0.12)
|
||||||
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.12 : 0.08)
|
property color primaryHoverLight: withAlpha(primary, transparentBlurLayers ? 0.12 : 0.08)
|
||||||
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.24 : 0.16)
|
property color primaryPressed: withAlpha(primary, transparentBlurLayers ? 0.24 : 0.16)
|
||||||
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
|
property color primarySelected: withAlpha(primary, 0.3)
|
||||||
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
|
property color primaryBackground: withAlpha(primary, 0.04)
|
||||||
|
|
||||||
property color secondaryHover: Qt.rgba(secondary.r, secondary.g, secondary.b, 0.08)
|
property color secondaryHover: withAlpha(secondary, 0.08)
|
||||||
|
|
||||||
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
|
property color surfaceHover: withAlpha(surfaceVariant, 0.08)
|
||||||
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
|
property color surfacePressed: withAlpha(surfaceVariant, 0.12)
|
||||||
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
|
property color surfaceSelected: withAlpha(surfaceVariant, 0.15)
|
||||||
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, transparentBlurLayers ? 0.3 : 0.1)
|
property color surfaceLight: withAlpha(surfaceVariant, transparentBlurLayers ? 0.3 : 0.1)
|
||||||
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
|
property color surfaceVariantAlpha: withAlpha(surfaceVariant, 0.2)
|
||||||
|
|
||||||
readonly property bool blurForegroundLayers: BlurService.enabled && (typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true))
|
readonly property bool foregroundLayers: typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true)
|
||||||
readonly property bool transparentBlurLayers: BlurService.enabled && !blurForegroundLayers
|
readonly property bool blurForegroundLayers: BlurService.enabled && foregroundLayers
|
||||||
|
readonly property bool transparentBlurLayers: BlurService.enabled && !foregroundLayers
|
||||||
readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
|
readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
|
||||||
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
|
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
|
||||||
readonly property color floatingSurface: transparentBlurLayers ? "transparent" : readableSurface
|
readonly property color floatingSurface: foregroundLayers ? readableSurface : withAlpha(readableSurface, 0)
|
||||||
readonly property color floatingSurfaceHigh: transparentBlurLayers ? "transparent" : readableSurfaceHigh
|
readonly property color floatingSurfaceHigh: foregroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0)
|
||||||
readonly property color nestedSurface: floatingSurfaceHigh
|
readonly property color nestedSurface: floatingSurfaceHigh
|
||||||
readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12)))
|
readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12)))
|
||||||
readonly property real layerOutlineOpacity: BlurService.enabled ? blurLayerOutlineOpacity : 0.08
|
readonly property real layerOutlineOpacity: blurLayerOutlineOpacity
|
||||||
readonly property int layerOutlineWidth: BlurService.enabled && layerOutlineOpacity > 0 ? 1 : 0
|
readonly property int layerOutlineWidth: layerOutlineOpacity > 0 ? 1 : 0
|
||||||
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
|
property color surfaceTextHover: withAlpha(surfaceText, 0.08)
|
||||||
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
|
property color surfaceTextAlpha: withAlpha(surfaceText, 0.3)
|
||||||
|
|
||||||
function roleColor(mode) {
|
function roleColor(mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
@@ -607,6 +614,10 @@ Singleton {
|
|||||||
return surfaceVariant;
|
return surfaceVariant;
|
||||||
case "s":
|
case "s":
|
||||||
return surface;
|
return surface;
|
||||||
|
case "scll":
|
||||||
|
return surfaceContainerLowest;
|
||||||
|
case "scl":
|
||||||
|
return surfaceContainerLow;
|
||||||
case "sc":
|
case "sc":
|
||||||
return surfaceContainer;
|
return surfaceContainer;
|
||||||
case "sch":
|
case "sch":
|
||||||
@@ -619,19 +630,23 @@ Singleton {
|
|||||||
case "err":
|
case "err":
|
||||||
return error;
|
return error;
|
||||||
default:
|
default:
|
||||||
return "transparent";
|
return withAlpha(surface, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
|
property color surfaceTextLight: withAlpha(surfaceText, 0.06)
|
||||||
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
|
property color surfaceTextSecondary: withAlpha(surfaceText, 0.6)
|
||||||
|
property color surfaceTextMedium: withAlpha(surfaceText, 0.7)
|
||||||
|
|
||||||
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
|
property color outlineButton: withAlpha(outline, 0.5)
|
||||||
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 0.625) : 0.05)
|
property color outlineLight: withAlpha(outline, Math.min(1, layerOutlineOpacity * 0.625))
|
||||||
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, layerOutlineOpacity)
|
property color outlineMedium: withAlpha(outline, layerOutlineOpacity)
|
||||||
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 1.5) : 0.12)
|
property color outlineStrong: withAlpha(outline, Math.min(1, layerOutlineOpacity * 1.5))
|
||||||
|
property color outlineHeavy: withAlpha(outline, 0.2)
|
||||||
|
|
||||||
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
property color errorHover: withAlpha(error, 0.12)
|
||||||
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
property color errorPressed: withAlpha(error, 0.16)
|
||||||
|
property color errorSelected: withAlpha(error, 0.3)
|
||||||
|
property color warningHover: withAlpha(warning, 0.12)
|
||||||
|
|
||||||
readonly property color ccTileActiveBg: {
|
readonly property color ccTileActiveBg: {
|
||||||
switch (SettingsData.controlCenterTileColorMode) {
|
switch (SettingsData.controlCenterTileColorMode) {
|
||||||
@@ -646,7 +661,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (blurForegroundLayers ? withAlpha(surfaceContainerHigh, Math.min(popupTransparency, 0.24)) : withAlpha(surfaceContainer, popupTransparency))
|
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (foregroundLayers ? withAlpha(surfaceContainerHigh, BlurService.enabled ? Math.min(popupTransparency, 0.24) : popupTransparency) : withAlpha(surfaceContainer, 0))
|
||||||
readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
|
readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
|
||||||
readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
|
readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
|
||||||
readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
|
readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
|
||||||
@@ -681,13 +696,13 @@ Singleton {
|
|||||||
readonly property color ccTileRing: {
|
readonly property color ccTileRing: {
|
||||||
switch (SettingsData.controlCenterTileColorMode) {
|
switch (SettingsData.controlCenterTileColorMode) {
|
||||||
case "primaryContainer":
|
case "primaryContainer":
|
||||||
return Qt.rgba(primary.r, primary.g, primary.b, 0.22);
|
return withAlpha(primary, 0.22);
|
||||||
case "secondary":
|
case "secondary":
|
||||||
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.22);
|
return withAlpha(surfaceText, 0.22);
|
||||||
case "surfaceVariant":
|
case "surfaceVariant":
|
||||||
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.22);
|
return withAlpha(surfaceText, 0.22);
|
||||||
default:
|
default:
|
||||||
return Qt.rgba(primaryText.r, primaryText.g, primaryText.b, 0.22);
|
return withAlpha(primaryText, 0.22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -720,11 +735,11 @@ Singleton {
|
|||||||
readonly property color buttonHover: {
|
readonly property color buttonHover: {
|
||||||
switch (SettingsData.buttonColorMode) {
|
switch (SettingsData.buttonColorMode) {
|
||||||
case "primaryContainer":
|
case "primaryContainer":
|
||||||
return Qt.rgba(primary.r, primary.g, primary.b, 0.12);
|
return withAlpha(primary, 0.12);
|
||||||
case "secondary":
|
case "secondary":
|
||||||
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.12);
|
return withAlpha(surfaceText, 0.12);
|
||||||
case "surfaceVariant":
|
case "surfaceVariant":
|
||||||
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.12);
|
return withAlpha(surfaceText, 0.12);
|
||||||
default:
|
default:
|
||||||
return primaryHover;
|
return primaryHover;
|
||||||
}
|
}
|
||||||
@@ -733,11 +748,11 @@ Singleton {
|
|||||||
readonly property color buttonPressed: {
|
readonly property color buttonPressed: {
|
||||||
switch (SettingsData.buttonColorMode) {
|
switch (SettingsData.buttonColorMode) {
|
||||||
case "primaryContainer":
|
case "primaryContainer":
|
||||||
return Qt.rgba(primary.r, primary.g, primary.b, 0.16);
|
return withAlpha(primary, 0.16);
|
||||||
case "secondary":
|
case "secondary":
|
||||||
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.16);
|
return withAlpha(surfaceText, 0.16);
|
||||||
case "surfaceVariant":
|
case "surfaceVariant":
|
||||||
return Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.16);
|
return withAlpha(surfaceText, 0.16);
|
||||||
default:
|
default:
|
||||||
return primaryPressed;
|
return primaryPressed;
|
||||||
}
|
}
|
||||||
@@ -1063,12 +1078,12 @@ Singleton {
|
|||||||
readonly property real connectedCornerRadius: {
|
readonly property real connectedCornerRadius: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined")
|
||||||
return 12;
|
return 12;
|
||||||
return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
|
return FrameTransitionState.effectiveConnectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
|
||||||
}
|
}
|
||||||
readonly property color connectedSurfaceColor: {
|
readonly property color connectedSurfaceColor: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined")
|
||||||
return withAlpha(surfaceContainer, popupTransparency);
|
return withAlpha(surfaceContainer, popupTransparency);
|
||||||
return isConnectedEffect ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : withAlpha(surfaceContainer, popupTransparency);
|
return isConnectedEffect ? withAlpha(SettingsData.effectiveFrameColor, SettingsData.frameOpacity) : withAlpha(surfaceContainer, popupTransparency);
|
||||||
}
|
}
|
||||||
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
|
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
|
||||||
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined") ? true : (!isConnectedEffect || SettingsData.frameBlurEnabled)
|
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined") ? true : (!isConnectedEffect || SettingsData.frameBlurEnabled)
|
||||||
@@ -1237,6 +1252,7 @@ Singleton {
|
|||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0;
|
return typeof SettingsData !== "undefined" ? SettingsData.fontScale : 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property real spacingXXS: 2
|
||||||
property real spacingXS: 4
|
property real spacingXS: 4
|
||||||
property real spacingS: 8
|
property real spacingS: 8
|
||||||
property real spacingM: 12
|
property real spacingM: 12
|
||||||
@@ -1468,7 +1484,7 @@ Singleton {
|
|||||||
property string currentThemeName: currentTheme
|
property string currentThemeName: currentTheme
|
||||||
|
|
||||||
function panelBackground() {
|
function panelBackground() {
|
||||||
return Qt.rgba(surfaceContainer.r, surfaceContainer.g, surfaceContainer.b, panelTransparency);
|
return withAlpha(surfaceContainer, panelTransparency);
|
||||||
}
|
}
|
||||||
|
|
||||||
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
|
property real notepadTransparency: SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : popupTransparency
|
||||||
@@ -1787,8 +1803,11 @@ Singleton {
|
|||||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
||||||
|
|
||||||
if (currentTheme === dynamic) {
|
if (currentTheme === dynamic) {
|
||||||
if (!rawWallpaperPath)
|
if (!rawWallpaperPath) {
|
||||||
|
log.warn("Auto theme has no wallpaper - skipping matugen, syncing portal mode only");
|
||||||
|
PortalService.setLightMode(isLight);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
|
const selectedMatugenType = (typeof SettingsData !== "undefined" && SettingsData.matugenScheme) ? SettingsData.matugenScheme : "scheme-tonal-spot";
|
||||||
const kind = rawWallpaperPath.startsWith("#") ? "hex" : "image";
|
const kind = rawWallpaperPath.startsWith("#") ? "hex" : "image";
|
||||||
setDesiredTheme(kind, rawWallpaperPath, isLight, iconTheme, selectedMatugenType, null);
|
setDesiredTheme(kind, rawWallpaperPath, isLight, iconTheme, selectedMatugenType, null);
|
||||||
@@ -1813,19 +1832,21 @@ Singleton {
|
|||||||
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
||||||
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
||||||
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||||
const accentId = storedDark.accent || darkDefaults.accent || "";
|
const darkAccentId = storedDark.accent || darkDefaults.accent || "";
|
||||||
|
const lightAccentId = storedLight.accent || lightDefaults.accent || "";
|
||||||
const darkFlavor = findVariant(customThemeRawData.variants.flavors, darkFlavorId);
|
const darkFlavor = findVariant(customThemeRawData.variants.flavors, darkFlavorId);
|
||||||
const lightFlavor = findVariant(customThemeRawData.variants.flavors, lightFlavorId);
|
const lightFlavor = findVariant(customThemeRawData.variants.flavors, lightFlavorId);
|
||||||
const accent = findAccent(customThemeRawData.variants.accents, accentId);
|
const darkAccent = findAccent(customThemeRawData.variants.accents, darkAccentId);
|
||||||
|
const lightAccent = findAccent(customThemeRawData.variants.accents, lightAccentId);
|
||||||
if (darkFlavor) {
|
if (darkFlavor) {
|
||||||
darkTheme = mergeColors(darkTheme, darkFlavor.dark || {});
|
darkTheme = mergeColors(darkTheme, darkFlavor.dark || {});
|
||||||
if (accent)
|
if (darkAccent)
|
||||||
darkTheme = mergeColors(darkTheme, accent[darkFlavor.id] || {});
|
darkTheme = mergeColors(darkTheme, darkAccent[darkFlavor.id] || {});
|
||||||
}
|
}
|
||||||
if (lightFlavor) {
|
if (lightFlavor) {
|
||||||
lightTheme = mergeColors(lightTheme, lightFlavor.light || {});
|
lightTheme = mergeColors(lightTheme, lightFlavor.light || {});
|
||||||
if (accent)
|
if (lightAccent)
|
||||||
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
lightTheme = mergeColors(lightTheme, lightAccent[lightFlavor.id] || {});
|
||||||
}
|
}
|
||||||
} else if (customThemeRawData.variants.options) {
|
} else if (customThemeRawData.variants.options) {
|
||||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
@@ -1978,6 +1999,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function withAlpha(c, a) {
|
function withAlpha(c, a) {
|
||||||
|
if (!c || c.r === undefined)
|
||||||
|
return Qt.rgba(0, 0, 0, 0);
|
||||||
return Qt.rgba(c.r, c.g, c.b, a);
|
return Qt.rgba(c.r, c.g, c.b, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1988,6 +2011,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function blendAlpha(c, a) {
|
function blendAlpha(c, a) {
|
||||||
|
if (!c || c.r === undefined)
|
||||||
|
return Qt.rgba(0, 0, 0, 0);
|
||||||
return Qt.rgba(c.r, c.g, c.b, c.a * a);
|
return Qt.rgba(c.r, c.g, c.b, c.a * a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ Singleton {
|
|||||||
showBatteryPercentOnlyOnBattery: false,
|
showBatteryPercentOnlyOnBattery: false,
|
||||||
showBatteryTime: false,
|
showBatteryTime: false,
|
||||||
showBatteryTimeOnlyOnBattery: false,
|
showBatteryTimeOnlyOnBattery: false,
|
||||||
|
batteryPillStyle: false,
|
||||||
|
batteryPillPercentSign: false,
|
||||||
showPrinterIcon: false,
|
showPrinterIcon: false,
|
||||||
showScreenSharingIcon: true
|
showScreenSharingIcon: true
|
||||||
};
|
};
|
||||||
@@ -95,6 +97,10 @@ Singleton {
|
|||||||
item.showBatteryTime = order[i].showBatteryTime;
|
item.showBatteryTime = order[i].showBatteryTime;
|
||||||
if (isObj && order[i].showBatteryTimeOnlyOnBattery !== undefined)
|
if (isObj && order[i].showBatteryTimeOnlyOnBattery !== undefined)
|
||||||
item.showBatteryTimeOnlyOnBattery = order[i].showBatteryTimeOnlyOnBattery;
|
item.showBatteryTimeOnlyOnBattery = order[i].showBatteryTimeOnlyOnBattery;
|
||||||
|
if (isObj && order[i].batteryPillStyle !== undefined)
|
||||||
|
item.batteryPillStyle = order[i].batteryPillStyle;
|
||||||
|
if (isObj && order[i].batteryPillPercentSign !== undefined)
|
||||||
|
item.batteryPillPercentSign = order[i].batteryPillPercentSign;
|
||||||
if (isObj && order[i].showPrinterIcon !== undefined)
|
if (isObj && order[i].showPrinterIcon !== undefined)
|
||||||
item.showPrinterIcon = order[i].showPrinterIcon;
|
item.showPrinterIcon = order[i].showPrinterIcon;
|
||||||
if (isObj && order[i].showScreenSharingIcon !== undefined)
|
if (isObj && order[i].showScreenSharingIcon !== undefined)
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ Singleton {
|
|||||||
property bool greeterAutoLoginSyncRerunRequested: false
|
property bool greeterAutoLoginSyncRerunRequested: false
|
||||||
property string greeterAutoLoginSyncStdout: ""
|
property string greeterAutoLoginSyncStdout: ""
|
||||||
property string greeterAutoLoginSyncStderr: ""
|
property string greeterAutoLoginSyncStderr: ""
|
||||||
property string greeterAutoLoginSyncTerminalFallbackStderr: ""
|
|
||||||
|
|
||||||
function scheduleGreeterAutoLoginSync() {
|
function scheduleGreeterAutoLoginSync() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot || settingsRoot.isGreeterMode)
|
||||||
@@ -355,15 +354,16 @@ Singleton {
|
|||||||
greeterAutoLoginSyncRerunRequested = false;
|
greeterAutoLoginSyncRerunRequested = false;
|
||||||
greeterAutoLoginSyncStdout = "";
|
greeterAutoLoginSyncStdout = "";
|
||||||
greeterAutoLoginSyncStderr = "";
|
greeterAutoLoginSyncStderr = "";
|
||||||
greeterAutoLoginSyncTerminalFallbackStderr = "";
|
|
||||||
greeterAutoLoginSyncRunning = true;
|
greeterAutoLoginSyncRunning = true;
|
||||||
greeterAutoLoginSyncSudoProbeProcess.running = true;
|
greeterAutoLoginSyncSudoProbeProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchGreeterAutoLoginSyncTerminalFallback(details) {
|
function deferGreeterAutoLoginSyncToPill(details) {
|
||||||
ToastService.showWarning(I18n.tr("Opening terminal to update greetd"), I18n.tr("DMS needs administrator access. The terminal closes automatically when done.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin", "greeter-autologin-sync");
|
ToastService.dismissCategory("greeter-autologin-sync");
|
||||||
greeterAutoLoginSyncTerminalFallbackStderr = "";
|
if (settingsRoot)
|
||||||
greeterAutoLoginSyncTerminalFallbackProcess.running = true;
|
settingsRoot.set("greeterSyncPending", true);
|
||||||
|
ToastService.showWarning(I18n.tr("Auto-login change needs a sync"), I18n.tr("Administrator access is required. Use the Sync button in Settings → Greeter to apply.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin", "greeter-autologin-sync");
|
||||||
|
finishGreeterAutoLoginSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
function greeterAutoLoginSyncSuccessToast(details) {
|
function greeterAutoLoginSyncSuccessToast(details) {
|
||||||
@@ -559,7 +559,7 @@ Singleton {
|
|||||||
details = out;
|
details = out;
|
||||||
if (err !== "")
|
if (err !== "")
|
||||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
||||||
root.launchGreeterAutoLoginSyncTerminalFallback(details);
|
root.deferGreeterAutoLoginSyncToPill(details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,26 +575,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
root.launchGreeterAutoLoginSyncTerminalFallback();
|
root.deferGreeterAutoLoginSyncToPill("");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var greeterAutoLoginSyncTerminalFallbackProcess: Process {
|
|
||||||
command: ["dms", "greeter", "sync", "--terminal", "--yes", "--autologin"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stderr: StdioCollector {
|
|
||||||
onStreamFinished: root.greeterAutoLoginSyncTerminalFallbackStderr = text || ""
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
root.greeterAutoLoginSyncSuccessToast("");
|
|
||||||
} else {
|
|
||||||
let details = (root.greeterAutoLoginSyncTerminalFallbackStderr || "").trim();
|
|
||||||
ToastService.showError(I18n.tr("Couldn't open a terminal for the auto-login update.") + " (exit " + exitCode + ")", details, "dms greeter sync --autologin", "greeter-autologin-sync");
|
|
||||||
}
|
|
||||||
root.finishGreeterAutoLoginSync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ var SPEC = {
|
|||||||
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
hyprlandLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
hyprlandLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
|
hyprlandLayoutGapsOutOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
hyprlandLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
hyprlandLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
hyprlandLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
hyprlandLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
hyprlandResizeOnBorder: { def: false, onChange: "updateCompositorLayout" },
|
hyprlandResizeOnBorder: { def: false, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
|
mangoLayoutGapsOutOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
|
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
|
||||||
@@ -100,6 +102,8 @@ var SPEC = {
|
|||||||
showBatteryPercentOnlyOnBattery: { def: false },
|
showBatteryPercentOnlyOnBattery: { def: false },
|
||||||
showBatteryTime: { def: false },
|
showBatteryTime: { def: false },
|
||||||
showBatteryTimeOnlyOnBattery: { def: false },
|
showBatteryTimeOnlyOnBattery: { def: false },
|
||||||
|
batteryPillStyle: { def: false },
|
||||||
|
batteryPillPercentSign: { def: false },
|
||||||
showControlCenterButton: { def: true },
|
showControlCenterButton: { def: true },
|
||||||
showCapsLockIndicator: { def: true },
|
showCapsLockIndicator: { def: true },
|
||||||
|
|
||||||
@@ -184,9 +188,11 @@ var SPEC = {
|
|||||||
audioScrollMode: { def: "volume" },
|
audioScrollMode: { def: "volume" },
|
||||||
audioWheelScrollAmount: { def: 5 },
|
audioWheelScrollAmount: { def: 5 },
|
||||||
audioDeviceScrollVolumeEnabled: { def: false },
|
audioDeviceScrollVolumeEnabled: { def: false },
|
||||||
|
mediaExcludePlayers: { def: [] },
|
||||||
clockCompactMode: { def: false },
|
clockCompactMode: { def: false },
|
||||||
focusedWindowCompactMode: { def: false },
|
focusedWindowCompactMode: { def: false },
|
||||||
focusedWindowSize: { def: 1 },
|
focusedWindowSize: { def: 1 },
|
||||||
|
focusedWindowShowIcon: { def: false },
|
||||||
runningAppsCompactMode: { def: true },
|
runningAppsCompactMode: { def: true },
|
||||||
barMaxVisibleApps: { def: 0 },
|
barMaxVisibleApps: { def: 0 },
|
||||||
barMaxVisibleRunningApps: { def: 0 },
|
barMaxVisibleRunningApps: { def: 0 },
|
||||||
@@ -217,18 +223,20 @@ var SPEC = {
|
|||||||
centeringMode: { def: "index" },
|
centeringMode: { def: "index" },
|
||||||
clockDateFormat: { def: "" },
|
clockDateFormat: { def: "" },
|
||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
greeterRememberLastSession: { def: true },
|
greeterRememberLastSession: { def: true, onChange: "markGreeterSyncPending" },
|
||||||
greeterRememberLastUser: { def: true },
|
greeterRememberLastUser: { def: true, onChange: "markGreeterSyncPending" },
|
||||||
greeterAutoLogin: { def: false, onChange: "scheduleGreeterAutoLoginSync" },
|
greeterAutoLogin: { def: false, onChange: "scheduleGreeterAutoLoginSync" },
|
||||||
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||||
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
greeterWallpaperPath: { def: "" },
|
greeterWallpaperPath: { def: "", onChange: "markGreeterSyncPending" },
|
||||||
greeterUse24HourClock: { def: true },
|
greeterUse24HourClock: { def: true, onChange: "markGreeterSyncPending" },
|
||||||
greeterShowSeconds: { def: false },
|
greeterShowSeconds: { def: false, onChange: "markGreeterSyncPending" },
|
||||||
greeterPadHours12Hour: { def: false },
|
greeterPadHours12Hour: { def: false, onChange: "markGreeterSyncPending" },
|
||||||
greeterLockDateFormat: { def: "" },
|
greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" },
|
||||||
greeterFontFamily: { def: "" },
|
greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" },
|
||||||
greeterWallpaperFillMode: { def: "" },
|
greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" },
|
||||||
|
greeterSyncPending: { def: false },
|
||||||
|
greeterSyncBaseline: { def: {} },
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
@@ -334,7 +342,9 @@ var SPEC = {
|
|||||||
batteryNotifyCritical: { def: true },
|
batteryNotifyCritical: { def: true },
|
||||||
batteryLowThreshold: { def: 20 },
|
batteryLowThreshold: { def: 20 },
|
||||||
batteryNotifyLow: { def: false },
|
batteryNotifyLow: { def: false },
|
||||||
batteryNotificationType: { def: 0 },
|
batteryChargeLimitNotificationType: { def: 0 },
|
||||||
|
batteryLowNotificationType: { def: 0 },
|
||||||
|
batteryCriticalNotificationType: { def: 1 },
|
||||||
batteryAutoPowerSaver: { def: false },
|
batteryAutoPowerSaver: { def: false },
|
||||||
lockBeforeSuspend: { def: false },
|
lockBeforeSuspend: { def: false },
|
||||||
loginctlLockIntegration: { def: true },
|
loginctlLockIntegration: { def: true },
|
||||||
@@ -446,12 +456,14 @@ var SPEC = {
|
|||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
u2fMode: { def: "or" },
|
u2fMode: { def: "or" },
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
lockScreenVideoEnabled: { def: false },
|
lockScreenVideoEnabled: { def: false },
|
||||||
lockScreenVideoPath: { def: "" },
|
lockScreenVideoPath: { def: "" },
|
||||||
lockScreenVideoCycling: { def: false },
|
lockScreenVideoCycling: { def: false },
|
||||||
|
lockScreenWallpaperPath: { def: "" },
|
||||||
|
lockScreenWallpaperFillMode: { def: "" },
|
||||||
|
lockScreenFontFamily: { def: "" },
|
||||||
hideBrightnessSlider: { def: false },
|
hideBrightnessSlider: { def: false },
|
||||||
|
|
||||||
notificationTimeoutLow: { def: 5000 },
|
notificationTimeoutLow: { def: 5000 },
|
||||||
@@ -569,7 +581,9 @@ var SPEC = {
|
|||||||
shadowOpacity: 60,
|
shadowOpacity: 60,
|
||||||
shadowColorMode: "default",
|
shadowColorMode: "default",
|
||||||
shadowCustomColor: "#000000",
|
shadowCustomColor: "#000000",
|
||||||
clickThrough: false
|
clickThrough: false,
|
||||||
|
hoverPopouts: false,
|
||||||
|
hoverPopoutDelay: 150
|
||||||
}], onChange: "updateBarConfigs"
|
}], onChange: "updateBarConfigs"
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -642,6 +656,7 @@ var SPEC = {
|
|||||||
frameCloseGaps: { def: true },
|
frameCloseGaps: { def: true },
|
||||||
frameLauncherEmergeSide: { def: "bottom" },
|
frameLauncherEmergeSide: { def: "bottom" },
|
||||||
frameLauncherArcExtender: { def: false },
|
frameLauncherArcExtender: { def: false },
|
||||||
|
frameLauncherEdgeHover: { def: false },
|
||||||
frameMode: { def: "connected" },
|
frameMode: { def: "connected" },
|
||||||
barInsetPaddingShared: { def: -1 },
|
barInsetPaddingShared: { def: -1 },
|
||||||
barInsetPaddingSyncAll: { def: false },
|
barInsetPaddingSyncAll: { def: false },
|
||||||
@@ -655,10 +670,11 @@ function getValidKeys() {
|
|||||||
function set(root, key, value, saveFn, hooks) {
|
function set(root, key, value, saveFn, hooks) {
|
||||||
if (!(key in SPEC)) return;
|
if (!(key in SPEC)) return;
|
||||||
if (value === undefined || value === null) value = SPEC[key].def;
|
if (value === undefined || value === null) value = SPEC[key].def;
|
||||||
|
var oldValue = root[key];
|
||||||
root[key] = value;
|
root[key] = value;
|
||||||
var hookName = SPEC[key].onChange;
|
var hookName = SPEC[key].onChange;
|
||||||
if (hookName && hooks && hooks[hookName]) {
|
if (hookName && hooks && hooks[hookName]) {
|
||||||
hooks[hookName](root);
|
hooks[hookName](root, key, oldValue);
|
||||||
}
|
}
|
||||||
saveFn();
|
saveFn();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,6 +252,17 @@ function migrateToVersion(obj, targetVersion) {
|
|||||||
settings.configVersion = 11;
|
settings.configVersion = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentVersion < 12) {
|
||||||
|
console.info("Migrating settings from version", currentVersion, "to version 12");
|
||||||
|
if (settings.batteryNotificationType !== undefined) {
|
||||||
|
settings.batteryChargeLimitNotificationType = settings.batteryNotificationType;
|
||||||
|
settings.batteryLowNotificationType = settings.batteryNotificationType;
|
||||||
|
settings.batteryCriticalNotificationType = settings.batteryNotificationType;
|
||||||
|
delete settings.batteryNotificationType;
|
||||||
|
}
|
||||||
|
settings.configVersion = 12;
|
||||||
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-7
@@ -173,6 +173,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool barSurfacesLoaded: true
|
property bool barSurfacesLoaded: true
|
||||||
|
property int pendingFrameTransitionRevision: 0
|
||||||
|
|
||||||
function recreateBarSurfaces() {
|
function recreateBarSurfaces() {
|
||||||
log.info("Recreating bar surfaces, screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","));
|
log.info("Recreating bar surfaces, screens:", Quickshell.screens.length, Quickshell.screens.map(s => s.name).join(","));
|
||||||
@@ -181,9 +182,23 @@ Item {
|
|||||||
barSurfaceReloadAction.schedule();
|
barSurfaceReloadAction.schedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Holds the bar rebuild until the compositor applies the layout, so the swap lands in one pass
|
||||||
|
function runPendingFrameTransition() {
|
||||||
|
if (pendingFrameTransitionRevision <= 0 || !CompositorService.frameCompositorLayoutReady)
|
||||||
|
return;
|
||||||
|
recreateBarSurfaces();
|
||||||
|
}
|
||||||
|
|
||||||
DeferredAction {
|
DeferredAction {
|
||||||
id: barSurfaceReloadAction
|
id: barSurfaceReloadAction
|
||||||
onTriggered: root.barSurfacesLoaded = true
|
onTriggered: {
|
||||||
|
// Ack first so the latch flips and new bars build directly in the post-transition state
|
||||||
|
if (root.pendingFrameTransitionRevision > 0 && CompositorService.frameCompositorLayoutReady) {
|
||||||
|
FrameTransitionState.acknowledge(root.pendingFrameTransitionRevision);
|
||||||
|
root.pendingFrameTransitionRevision = 0;
|
||||||
|
}
|
||||||
|
root.barSurfacesLoaded = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property string _barLayoutStateJson: {
|
property string _barLayoutStateJson: {
|
||||||
@@ -212,14 +227,23 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: FrameTransitionState
|
||||||
|
function onTransitionRequested(revision) {
|
||||||
|
root.pendingFrameTransitionRevision = Math.max(root.pendingFrameTransitionRevision, revision);
|
||||||
|
root.runPendingFrameTransition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: CompositorService
|
||||||
|
function onFrameCompositorLayoutReadyChanged() {
|
||||||
|
root.runPendingFrameTransition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
function onFrameEnabledChanged() {
|
|
||||||
root.recreateBarSurfaces();
|
|
||||||
}
|
|
||||||
function onConnectedFrameModeActiveChanged() {
|
|
||||||
root.recreateBarSurfaces();
|
|
||||||
}
|
|
||||||
function onForceDankBarLayoutRefresh() {
|
function onForceDankBarLayoutRefresh() {
|
||||||
root.recreateBarSurfaces();
|
root.recreateBarSurfaces();
|
||||||
}
|
}
|
||||||
@@ -233,6 +257,12 @@ Item {
|
|||||||
sourceComponent: Frame {}
|
sourceComponent: Frame {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: FrameTransitionState.effectiveFrameEnabled && SettingsData.frameLauncherEdgeHover
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: FrameLauncherHoverZone {}
|
||||||
|
}
|
||||||
|
|
||||||
DeferredAction {
|
DeferredAction {
|
||||||
id: frameSurfaceReloadAction
|
id: frameSurfaceReloadAction
|
||||||
onTriggered: root.frameSurfacesLoaded = true
|
onTriggered: root.frameSurfacesLoaded = true
|
||||||
@@ -245,6 +275,8 @@ Item {
|
|||||||
values: JSON.parse(root._barLayoutStateJson)
|
values: JSON.parse(root._barLayoutStateJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: BarWidgetService.dankBarRepeater = dankBarRepeater
|
||||||
|
|
||||||
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
|
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
|
||||||
|
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
|
|||||||
@@ -385,6 +385,22 @@ Item {
|
|||||||
return "NOTEPAD_OPEN_FAILED";
|
return "NOTEPAD_OPEN_FAILED";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openFile(path: string): string {
|
||||||
|
if (!path)
|
||||||
|
return open();
|
||||||
|
if (SettingsData.notepadDefaultMode === "popout") {
|
||||||
|
PopoutService.openNotepadPopoutWithFile(path);
|
||||||
|
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
||||||
|
}
|
||||||
|
var instance = getActiveNotepadInstance();
|
||||||
|
if (instance) {
|
||||||
|
instance.show();
|
||||||
|
instance.loadedItem?.openExternalFile(path);
|
||||||
|
return "NOTEPAD_OPEN_FILE_SUCCESS";
|
||||||
|
}
|
||||||
|
return "NOTEPAD_OPEN_FILE_FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
if (SettingsData.notepadDefaultMode === "popout") {
|
if (SettingsData.notepadDefaultMode === "popout") {
|
||||||
PopoutService.notepadPopout?.hide();
|
PopoutService.notepadPopout?.hide();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -23,6 +24,7 @@ DankModal {
|
|||||||
property var rememberMimeTypes: []
|
property var rememberMimeTypes: []
|
||||||
property bool rememberChoice: false
|
property bool rememberChoice: false
|
||||||
property var mimeMatchedAppIds: []
|
property var mimeMatchedAppIds: []
|
||||||
|
property var mimeMatchedRawIds: []
|
||||||
|
|
||||||
signal applicationSelected(var app, string targetData)
|
signal applicationSelected(var app, string targetData)
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ DankModal {
|
|||||||
|
|
||||||
function fetchMimeMatches() {
|
function fetchMimeMatches() {
|
||||||
mimeMatchedAppIds = [];
|
mimeMatchedAppIds = [];
|
||||||
|
mimeMatchedRawIds = [];
|
||||||
const queriedMime = mimeType;
|
const queriedMime = mimeType;
|
||||||
if (queriedMime.length === 0)
|
if (queriedMime.length === 0)
|
||||||
return;
|
return;
|
||||||
@@ -74,6 +77,7 @@ DankModal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ids = (response.result && response.result.desktopIds) || [];
|
const ids = (response.result && response.result.desktopIds) || [];
|
||||||
|
mimeMatchedRawIds = ids;
|
||||||
mimeMatchedAppIds = ids.map(_normAppId);
|
mimeMatchedAppIds = ids.map(_normAppId);
|
||||||
updateApplicationList();
|
updateApplicationList();
|
||||||
});
|
});
|
||||||
@@ -93,11 +97,13 @@ DankModal {
|
|||||||
const hasMimeMatches = mimeMatchedAppIds.length > 0;
|
const hasMimeMatches = mimeMatchedAppIds.length > 0;
|
||||||
const lowerQuery = searchQuery.toLowerCase();
|
const lowerQuery = searchQuery.toLowerCase();
|
||||||
let filteredApps = [];
|
let filteredApps = [];
|
||||||
|
const listedIds = new Set();
|
||||||
|
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
if (!app)
|
if (!app)
|
||||||
continue;
|
continue;
|
||||||
const appId = _normAppId(app.id || app.execString || app.exec || "");
|
const appId = _normAppId(app.id || app.execString || app.exec || "");
|
||||||
|
listedIds.add(appId);
|
||||||
const mimeIdMatch = hasMimeMatches && mimeMatchedAppIds.includes(appId);
|
const mimeIdMatch = hasMimeMatches && mimeMatchedAppIds.includes(appId);
|
||||||
const mimeFieldMatch = hasMime && _appMatchesMime(app, mimeType);
|
const mimeFieldMatch = hasMime && _appMatchesMime(app, mimeType);
|
||||||
const mimeMatch = mimeIdMatch || mimeFieldMatch;
|
const mimeMatch = mimeIdMatch || mimeFieldMatch;
|
||||||
@@ -135,6 +141,28 @@ DankModal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NoDisplay entries are excluded from DesktopEntries.applications but
|
||||||
|
// remain valid mime handlers; resolve them by id so they stay pickable
|
||||||
|
for (const rawId of mimeMatchedRawIds) {
|
||||||
|
const normId = _normAppId(rawId);
|
||||||
|
if (normId === "dms-open" || listedIds.has(normId))
|
||||||
|
continue;
|
||||||
|
const entry = DesktopEntries.byId(rawId) || DesktopEntries.heuristicLookup(rawId);
|
||||||
|
if (!entry)
|
||||||
|
continue;
|
||||||
|
const name = entry.name || "";
|
||||||
|
if (searchQuery !== "" && !name.toLowerCase().includes(lowerQuery))
|
||||||
|
continue;
|
||||||
|
filteredApps.push({
|
||||||
|
name: name,
|
||||||
|
icon: entry.icon || "application-x-executable",
|
||||||
|
exec: entry.execString || "",
|
||||||
|
startupClass: entry.startupClass || "",
|
||||||
|
appData: entry,
|
||||||
|
mimeMatch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
filteredApps.sort((a, b) => {
|
filteredApps.sort((a, b) => {
|
||||||
if (a.mimeMatch !== b.mimeMatch) {
|
if (a.mimeMatch !== b.mimeMatch) {
|
||||||
return a.mimeMatch ? -1 : 1;
|
return a.mimeMatch ? -1 : 1;
|
||||||
@@ -253,7 +281,7 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: 4
|
spacing: Theme.spacingXS
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: Theme.spacingS
|
anchors.rightMargin: Theme.spacingS
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -264,7 +292,7 @@ DankModal {
|
|||||||
iconName: "view_list"
|
iconName: "view_list"
|
||||||
iconSize: 20
|
iconSize: 20
|
||||||
iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||||
backgroundColor: root.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
backgroundColor: root.viewMode === "list" ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.viewMode = "list";
|
root.viewMode = "list";
|
||||||
}
|
}
|
||||||
@@ -276,7 +304,7 @@ DankModal {
|
|||||||
iconName: "grid_view"
|
iconName: "grid_view"
|
||||||
iconSize: 20
|
iconSize: 20
|
||||||
iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||||
backgroundColor: root.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
backgroundColor: root.viewMode === "grid" ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.viewMode = "grid";
|
root.viewMode = "grid";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ DankModal {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 2
|
spacing: Theme.spacingXXS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Passkey:")
|
text: I18n.tr("Passkey:")
|
||||||
@@ -256,7 +256,7 @@ DankModal {
|
|||||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||||
height: 36
|
height: 36
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
|
color: cancelArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0)
|
||||||
border.color: Theme.surfaceVariantAlpha
|
border.color: Theme.surfaceVariantAlpha
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Rectangle {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 2
|
spacing: Theme.spacingXXS
|
||||||
width: parent.width - root.iconContainerSize - Theme.spacingS
|
width: parent.width - root.iconContainerSize - Theme.spacingS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ FloatingWindow {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
|
onClosed: visible = false
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: contentFocusScope
|
id: contentFocusScope
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Item {
|
|||||||
id: clipboardContent
|
id: clipboardContent
|
||||||
|
|
||||||
required property var modal
|
required property var modal
|
||||||
|
property var transientSurfaceTracker: null
|
||||||
|
|
||||||
property alias searchField: searchField
|
property alias searchField: searchField
|
||||||
property alias clipboardListView: clipboardListView
|
property alias clipboardListView: clipboardListView
|
||||||
@@ -66,7 +67,7 @@ Item {
|
|||||||
contextMenu.hide();
|
contextMenu.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool contextMenuActive: contextMenu.openState
|
readonly property bool contextMenuActive: contextMenu.renderActive
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ Item {
|
|||||||
id: contextMenu
|
id: contextMenu
|
||||||
modal: clipboardContent.modal
|
modal: clipboardContent.modal
|
||||||
parentHandler: clipboardContent
|
parentHandler: clipboardContent
|
||||||
|
transientSurfaceTracker: clipboardContent.transientSurfaceTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -147,7 +149,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "filter_list"
|
iconName: "filter_list"
|
||||||
iconColor: modal.activeFilter !== "all" ? Theme.primary : Theme.surfaceText
|
iconColor: modal.activeFilter !== "all" ? Theme.primary : Theme.surfaceText
|
||||||
backgroundColor: modal.activeFilter !== "all" ? Theme.primarySelected : "transparent"
|
backgroundColor: modal.activeFilter !== "all" ? Theme.primarySelected : Theme.withAlpha(Theme.primarySelected, 0)
|
||||||
tooltipText: I18n.tr("Filter by type", "Clipboard history type filter button tooltip")
|
tooltipText: I18n.tr("Filter by type", "Clipboard history type filter button tooltip")
|
||||||
onClicked: filterMenuLoader.item?.openDropdownMenu()
|
onClicked: filterMenuLoader.item?.openDropdownMenu()
|
||||||
}
|
}
|
||||||
@@ -394,7 +396,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceComponent: ClipboardKeyboardHints {
|
sourceComponent: ClipboardKeyboardHints {
|
||||||
wtypeAvailable: modal.wtypeAvailable
|
pasteAvailable: modal.pasteAvailable
|
||||||
enterToPaste: SettingsData.clipboardEnterToPaste
|
enterToPaste: SettingsData.clipboardEnterToPaste
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ Item {
|
|||||||
property real anchorY: 0
|
property real anchorY: 0
|
||||||
property bool openState: false
|
property bool openState: false
|
||||||
property bool renderActive: false
|
property bool renderActive: false
|
||||||
|
property var transientSurfaceTracker: null
|
||||||
|
readonly property alias contextWindow: menuWindow
|
||||||
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
||||||
|
|
||||||
readonly property bool hasPinnedDuplicate: !!entry && !entry.pinned && ClipboardService.getPinnedEntryByHash(entry.hash) !== null
|
readonly property bool hasPinnedDuplicate: !!entry && !entry.pinned && ClipboardService.getPinnedEntryByHash(entry.hash) !== null
|
||||||
@@ -90,6 +92,18 @@ Item {
|
|||||||
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
|
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
|
||||||
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
|
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
|
||||||
|
|
||||||
|
onRenderActiveChanged: transientSurfaceTracker?.setActive(root, renderActive, contextWindow)
|
||||||
|
Component.onDestruction: transientSurfaceTracker?.unregister(root)
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.transientSurfaceTracker
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
|
||||||
|
function onCloseRequested() {
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: menuTextMetrics
|
id: menuTextMetrics
|
||||||
text: root.longestMenuText
|
text: root.longestMenuText
|
||||||
@@ -258,8 +272,8 @@ Item {
|
|||||||
height: root.effectiveMenuHeight
|
height: root.effectiveMenuHeight
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: BlurService.borderColor
|
||||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
border.width: BlurService.borderWidth
|
||||||
opacity: root.openState ? 1 : 0
|
opacity: root.openState ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -321,7 +335,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 1
|
height: 1
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
color: Theme.outlineHeavy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +344,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
color: itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(BlurService.hoverColor(Theme.widgetBaseHoverColor), 0)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ Item {
|
|||||||
implicitWidth: saveMenuRow.implicitWidth + Theme.spacingS * 2
|
implicitWidth: saveMenuRow.implicitWidth + Theme.spacingS * 2
|
||||||
implicitHeight: saveMenuRow.implicitHeight + Theme.spacingS * 2
|
implicitHeight: saveMenuRow.implicitHeight + Theme.spacingS * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : Theme.withAlpha(Theme.surfaceVariant, 0)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: saveMenuRow
|
id: saveMenuRow
|
||||||
@@ -457,7 +457,7 @@ Item {
|
|||||||
implicitWidth: saveMenuCloseRow.implicitWidth + Theme.spacingS * 2
|
implicitWidth: saveMenuCloseRow.implicitWidth + Theme.spacingS * 2
|
||||||
implicitHeight: saveMenuCloseRow.implicitHeight + Theme.spacingS * 2
|
implicitHeight: saveMenuCloseRow.implicitHeight + Theme.spacingS * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : Theme.withAlpha(Theme.surfaceVariant, 0)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: saveMenuCloseRow
|
id: saveMenuCloseRow
|
||||||
@@ -495,8 +495,8 @@ Item {
|
|||||||
implicitWidth: saveMenuPasteRow.implicitWidth + Theme.spacingS * 2
|
implicitWidth: saveMenuPasteRow.implicitWidth + Theme.spacingS * 2
|
||||||
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
|
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : Theme.withAlpha(Theme.surfaceVariant, 0)
|
||||||
opacity: modal.wtypeAvailable ? 1 : 0.5
|
opacity: modal.pasteAvailable ? 1 : 0.5
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: saveMenuPasteRow
|
id: saveMenuPasteRow
|
||||||
@@ -522,7 +522,7 @@ Item {
|
|||||||
id: saveMenuPasteArea
|
id: saveMenuPasteArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
enabled: modal.wtypeAvailable
|
enabled: modal.pasteAvailable
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
saveMenu.close();
|
saveMenu.close();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user