mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Compare commits
118 Commits
cbd1fd908c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aedeab8a6a | ||
|
|
4d39169eb8 | ||
|
|
2ddc448150 | ||
|
|
f9a6b4ce2c | ||
|
|
22b2b69413 | ||
|
|
7f11632ea6 | ||
|
|
c0b4d5e2c2 | ||
|
|
2c23d0249c | ||
|
|
c3233fbf61 | ||
|
|
ecfc8e208c | ||
|
|
52d5e21fc4 | ||
|
|
6d0c56554f | ||
|
|
844e91dc9e | ||
|
|
1f00b5f577 | ||
|
|
2c48458384 | ||
|
|
ddda87c5a7 | ||
|
|
6b1bbca620 | ||
|
|
b5378e5d3c | ||
|
|
c69a55df29 | ||
|
|
5faa1a993a | ||
|
|
e56481f6d7 | ||
|
|
f9610d457c | ||
|
|
ae066f42a4 | ||
|
|
c60dd42fa7 | ||
|
|
7aac5ac5a1 | ||
|
|
ad0f3fa33b | ||
|
|
63d121b796 | ||
|
|
4291cfe82f | ||
|
|
f312868154 | ||
|
|
5b42d34ac8 | ||
|
|
397a8c275d | ||
|
|
2aabee453b | ||
|
|
185333a615 | ||
|
|
7d177eb1d4 | ||
|
|
705a84051d | ||
|
|
f6821f80e1 | ||
|
|
e7a6f5228d | ||
|
|
8161fd6acb | ||
|
|
2137920e81 | ||
|
|
879102599c | ||
|
|
44190f07fe | ||
|
|
a41487eb8f | ||
|
|
e1acaaa27c | ||
|
|
08a97aeff8 | ||
|
|
5b7302b46d | ||
|
|
34c0bba130 | ||
|
|
5a53447272 | ||
|
|
b6847289ff | ||
|
|
d22c43e08b | ||
|
|
d9deaa8d74 | ||
|
|
6c7776a9a6 | ||
|
|
62bd6e41ef | ||
|
|
293c7b42c6 | ||
|
|
788da62777 | ||
|
|
2c7f24a913 | ||
|
|
f236706d6a | ||
|
|
b097700591 | ||
|
|
50b112c9d6 | ||
|
|
c2f478b088 | ||
|
|
dccbb137d7 | ||
|
|
90f9940dbd | ||
|
|
f3f7cc9077 | ||
|
|
c331e2f39e | ||
|
|
1c7ebc4323 | ||
|
|
5f5427266f | ||
|
|
33e655becd | ||
|
|
0ea0602aec | ||
|
|
46effd2ca4 | ||
|
|
de055e8260 | ||
|
|
c3077304af | ||
|
|
e15135911f | ||
|
|
d430cae944 | ||
|
|
f92dc6f71b | ||
|
|
a679be68b1 | ||
|
|
c5c5ce8409 | ||
|
|
e7cb0d397e | ||
|
|
b84308cb49 | ||
|
|
0df47d2ce3 | ||
|
|
e24b548b54 | ||
|
|
75af444cee | ||
|
|
02dd19962f | ||
|
|
f552b8ef7b | ||
|
|
9162e31489 | ||
|
|
01b28e3ee8 | ||
|
|
f5aa855125 | ||
|
|
db3610fcdb | ||
|
|
2e3f330058 | ||
|
|
1617a7f2c1 | ||
|
|
69a5566bf9 | ||
|
|
30e5d8b855 | ||
|
|
67ff7726e0 | ||
|
|
f96a2e2325 | ||
|
|
344c4f9385 | ||
|
|
89aa146845 | ||
|
|
468e569bc7 | ||
|
|
139c99001a | ||
|
|
bd99be15c2 | ||
|
|
1d91d8fd94 | ||
|
|
f425f86101 | ||
|
|
83a6b7567f | ||
|
|
9184c70883 | ||
|
|
f5ca4ccce5 | ||
|
|
50f174be92 | ||
|
|
e5d11ce535 | ||
|
|
94851a51aa | ||
|
|
cfc07f4411 | ||
|
|
c6e9abda9f | ||
|
|
25951ddc55 | ||
|
|
bcd9ece077 | ||
|
|
68adbc38ba | ||
|
|
79a4d06cc0 | ||
|
|
18bf3b7548 | ||
|
|
4e66d3532e | ||
|
|
1b6d567451 | ||
|
|
7959a79575 | ||
|
|
abf3249b67 | ||
|
|
35e0dc84e8 | ||
|
|
17639e8729 |
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
@@ -12,42 +12,45 @@ cd "$REPO_ROOT"
|
|||||||
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
||||||
|
|
||||||
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
||||||
echo "Go files staged in core/, running CI checks..."
|
echo "Go files staged in core/, running CI checks..."
|
||||||
cd "$REPO_ROOT/core"
|
cd "$REPO_ROOT/core"
|
||||||
|
|
||||||
# Format check
|
# Format check
|
||||||
echo " Checking gofmt..."
|
echo " Checking gofmt..."
|
||||||
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
||||||
if [[ -n "$UNFORMATTED" ]]; then
|
if [[ -n "$UNFORMATTED" ]]; then
|
||||||
echo "The following files are not formatted:"
|
echo "The following files are not formatted:"
|
||||||
echo "$UNFORMATTED"
|
echo "$UNFORMATTED"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Run: cd core && gofmt -s -w ."
|
echo "Run: cd core && gofmt -s -w ."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# golangci-lint
|
# golangci-lint
|
||||||
if command -v golangci-lint &>/dev/null; then
|
if command -v golangci-lint &>/dev/null; then
|
||||||
echo " Running golangci-lint..."
|
echo " Running golangci-lint..."
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
else
|
else
|
||||||
echo " Warning: golangci-lint not installed, skipping lint"
|
echo " Warning: golangci-lint not installed, skipping lint"
|
||||||
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
echo " Running tests..."
|
echo " Running tests..."
|
||||||
go test ./... > /dev/null
|
if ! go test ./... >/dev/null 2>&1; then
|
||||||
|
echo "Tests failed! Run 'go test ./...' for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Build checks
|
# Build checks
|
||||||
echo " Building..."
|
echo " Building..."
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
go build -buildvcs=false -o bin/dms ./cmd/dms
|
go build -buildvcs=false -o bin/dms ./cmd/dms
|
||||||
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
||||||
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
||||||
|
|
||||||
echo "All Go CI checks passed!"
|
echo "All Go CI checks passed!"
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
14
.github/workflows/go-ci.yml
vendored
14
.github/workflows/go-ci.yml
vendored
@@ -3,15 +3,15 @@ name: Go CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- "**"
|
||||||
paths:
|
paths:
|
||||||
- 'core/**'
|
- "core/**"
|
||||||
- '.github/workflows/go-ci.yml'
|
- ".github/workflows/go-ci.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main]
|
||||||
paths:
|
paths:
|
||||||
- 'core/**'
|
- "core/**"
|
||||||
- '.github/workflows/go-ci.yml'
|
- ".github/workflows/go-ci.yml"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: go-ci-${{ github.ref }}
|
group: go-ci-${{ github.ref }}
|
||||||
@@ -42,9 +42,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run golangci-lint
|
- name: Run golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: v2.6
|
||||||
working-directory: core
|
working-directory: core
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
|
|||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -517,7 +517,6 @@ jobs:
|
|||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
Recommends: cliphist
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: hyprpicker
|
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: wl-clipboard
|
Recommends: wl-clipboard
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
@@ -597,7 +596,10 @@ jobs:
|
|||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
|
|
||||||
|
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
|
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
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
@@ -607,6 +609,8 @@ jobs:
|
|||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
|
|
||||||
|
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
||||||
|
|
||||||
%posttrans
|
%posttrans
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
@@ -623,6 +627,8 @@ jobs:
|
|||||||
%doc README.md CONTRIBUTING.md
|
%doc README.md CONTRIBUTING.md
|
||||||
%{_datadir}/quickshell/dms/
|
%{_datadir}/quickshell/dms/
|
||||||
%{_userunitdir}/dms.service
|
%{_userunitdir}/dms.service
|
||||||
|
%{_datadir}/applications/dms-open.desktop
|
||||||
|
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
%files -n dms-cli
|
%files -n dms-cli
|
||||||
%{_bindir}/dms
|
%{_bindir}/dms
|
||||||
|
|||||||
119
.github/workflows/run-obs.yml
vendored
119
.github/workflows/run-obs.yml
vendored
@@ -61,56 +61,44 @@ jobs:
|
|||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
echo "Checking if dms-git source has changed..."
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
# Get latest commit hash from master branch
|
# Get current commit hash (8 chars to match spec format)
|
||||||
LATEST_COMMIT=$(git rev-parse origin/master 2>/dev/null || git rev-parse master 2>/dev/null || echo "")
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
if [[ -z "$LATEST_COMMIT" ]]; then
|
# Check OBS for last uploaded commit
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||||
echo "Could not determine git commit, proceeding with update"
|
mkdir -p "$OBS_BASE"
|
||||||
else
|
OBS_PROJECT="home:AvengeMedia:dms-git"
|
||||||
# Check OBS for last uploaded commit
|
|
||||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
||||||
mkdir -p "$OBS_BASE"
|
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
||||||
OBS_PROJECT="home:AvengeMedia:dms-git"
|
osc up -q 2>/dev/null || true
|
||||||
|
|
||||||
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
||||||
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
if [[ -f "dms-git.spec" ]]; then
|
||||||
osc up -q 2>/dev/null || true
|
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
# Check tarball age - if older than 3 hours, update needed
|
if [[ -n "$OBS_COMMIT" ]]; then
|
||||||
if [[ -f "dms-git-source.tar.gz" ]]; then
|
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||||
TARBALL_MTIME=$(stat -c%Y "dms-git-source.tar.gz" 2>/dev/null || echo "0")
|
|
||||||
CURRENT_TIME=$(date +%s)
|
|
||||||
AGE_SECONDS=$((CURRENT_TIME - TARBALL_MTIME))
|
|
||||||
AGE_HOURS=$((AGE_SECONDS / 3600))
|
|
||||||
|
|
||||||
# If tarball is older than 3 hours, check for new commits
|
|
||||||
if [[ $AGE_HOURS -ge 3 ]]; then
|
|
||||||
# Check if there are new commits in the last 3 hours
|
|
||||||
cd "${{ github.workspace }}"
|
|
||||||
NEW_COMMITS=$(git log --since="3 hours ago" --oneline origin/master 2>/dev/null | wc -l)
|
|
||||||
|
|
||||||
if [[ $NEW_COMMITS -gt 0 ]]; then
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 New commits detected in last 3 hours, update needed"
|
|
||||||
else
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No new commits in last 3 hours, skipping update"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
echo "📋 Recent upload exists (< 3 hours), skipping update"
|
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
echo "📋 No existing tarball in OBS, update needed"
|
echo "📋 Could not extract OBS commit, proceeding with update"
|
||||||
fi
|
fi
|
||||||
cd "${{ github.workspace }}"
|
|
||||||
else
|
else
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
echo "📋 First upload to OBS, update needed"
|
echo "📋 No spec file in OBS, proceeding with update"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cd "${{ github.workspace }}"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 First upload to OBS, update needed"
|
||||||
fi
|
fi
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
@@ -171,7 +159,52 @@ jobs:
|
|||||||
DATE_STR=$(date "+%a %b %d %Y")
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
|
- name: Update Debian dms-git changelog version
|
||||||
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
|
run: |
|
||||||
|
# Get commit info for dms-git versioning
|
||||||
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
||||||
|
|
||||||
|
# Debian version format: 0.6.2+git2256.9162e314
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
|
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
|
||||||
|
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
# Get current version from changelog
|
||||||
|
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
||||||
|
|
||||||
|
echo "Current Debian version: $CURRENT_VERSION"
|
||||||
|
echo "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Only update if version changed
|
||||||
|
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
||||||
|
# Create new changelog entry at top
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
|
||||||
|
cat > "$TEMP_CHANGELOG" << EOF
|
||||||
|
dms-git ($NEW_VERSION) nightly; urgency=medium
|
||||||
|
|
||||||
|
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
||||||
|
|
||||||
|
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Prepend to existing changelog
|
||||||
|
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
||||||
|
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
||||||
|
else
|
||||||
|
echo "✓ Debian changelog already at version $NEW_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Update dms stable version
|
- name: Update dms stable version
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
75
.github/workflows/run-ppa.yml
vendored
75
.github/workflows/run-ppa.yml
vendored
@@ -15,9 +15,70 @@ on:
|
|||||||
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload-ppa:
|
check-updates:
|
||||||
|
name: Check for updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check for updates
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "Checking if dms-git source has changed..."
|
||||||
|
|
||||||
|
# Get current commit hash (8 chars to match changelog format)
|
||||||
|
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
|
# Extract commit hash from changelog
|
||||||
|
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
||||||
|
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
||||||
|
|
||||||
|
if [[ -f "$CHANGELOG_FILE" ]]; then
|
||||||
|
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "📋 No changelog file found, proceeding with upload"
|
||||||
|
fi
|
||||||
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
else
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
upload-ppa:
|
||||||
|
name: Upload to PPA
|
||||||
|
needs: check-updates
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.check-updates.outputs.has_updates == 'true'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -54,13 +115,13 @@ jobs:
|
|||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by schedule: uploading git package"
|
echo "Triggered by schedule: uploading git package"
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
else
|
else
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload to PPA
|
- name: Upload to PPA
|
||||||
@@ -102,7 +163,11 @@ jobs:
|
|||||||
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
||||||
|
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
if [[ "$PACKAGES" == "all" ]]; then
|
||||||
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
156
Makefile
Normal file
156
Makefile
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Root Makefile for DankMaterialShell (DMS)
|
||||||
|
# Orchestrates building, installation, and systemd management
|
||||||
|
|
||||||
|
# Build configuration
|
||||||
|
BINARY_NAME=dms
|
||||||
|
CORE_DIR=core
|
||||||
|
BUILD_DIR=$(CORE_DIR)/bin
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
INSTALL_DIR=$(PREFIX)/bin
|
||||||
|
DATA_DIR=$(PREFIX)/share
|
||||||
|
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
||||||
|
|
||||||
|
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
||||||
|
SYSTEMD_USER_DIR=$(USER_HOME)/.config/systemd/user
|
||||||
|
|
||||||
|
SHELL_DIR=quickshell
|
||||||
|
SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
||||||
|
ASSETS_DIR=assets
|
||||||
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
|
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
build:
|
||||||
|
@echo "Building $(BINARY_NAME)..."
|
||||||
|
@$(MAKE) -C $(CORE_DIR) build
|
||||||
|
@echo "Build complete"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
# Installation targets
|
||||||
|
install-bin:
|
||||||
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Binary installed"
|
||||||
|
|
||||||
|
install-shell:
|
||||||
|
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||||
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
|
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
|
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||||
|
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
||||||
|
@echo "Shell files installed"
|
||||||
|
|
||||||
|
install-completions:
|
||||||
|
@echo "Installing shell completions..."
|
||||||
|
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
||||||
|
@mkdir -p $(DATA_DIR)/zsh/site-functions
|
||||||
|
@mkdir -p $(DATA_DIR)/fish/vendor_completions.d
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion bash > $(DATA_DIR)/bash-completion/completions/dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion zsh > $(DATA_DIR)/zsh/site-functions/_dms 2>/dev/null || true
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) completion fish > $(DATA_DIR)/fish/vendor_completions.d/dms.fish 2>/dev/null || true
|
||||||
|
@echo "Shell completions installed"
|
||||||
|
|
||||||
|
install-systemd:
|
||||||
|
@echo "Installing systemd user service..."
|
||||||
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR); fi
|
||||||
|
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):$(SUDO_USER) $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
|
||||||
|
install-icon:
|
||||||
|
@echo "Installing icon..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/danklogo.svg $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon installed"
|
||||||
|
|
||||||
|
install-desktop:
|
||||||
|
@echo "Installing desktop entry..."
|
||||||
|
@install -D -m 644 $(ASSETS_DIR)/dms-open.desktop $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry installed"
|
||||||
|
|
||||||
|
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
|
@echo ""
|
||||||
|
@echo "Installation complete!"
|
||||||
|
@echo ""
|
||||||
|
@echo "=== Cheers, the DMS Team! ==="
|
||||||
|
|
||||||
|
# Uninstallation targets
|
||||||
|
uninstall-bin:
|
||||||
|
@echo "Removing $(BINARY_NAME) from $(INSTALL_DIR)..."
|
||||||
|
@rm -f $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
|
@echo "Binary removed"
|
||||||
|
|
||||||
|
uninstall-shell:
|
||||||
|
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
||||||
|
@rm -rf $(SHELL_INSTALL_DIR)
|
||||||
|
@echo "Shell files removed"
|
||||||
|
|
||||||
|
uninstall-completions:
|
||||||
|
@echo "Removing shell completions..."
|
||||||
|
@rm -f $(DATA_DIR)/bash-completion/completions/dms
|
||||||
|
@rm -f $(DATA_DIR)/zsh/site-functions/_dms
|
||||||
|
@rm -f $(DATA_DIR)/fish/vendor_completions.d/dms.fish
|
||||||
|
@echo "Shell completions removed"
|
||||||
|
|
||||||
|
uninstall-systemd:
|
||||||
|
@echo "Removing systemd user service..."
|
||||||
|
@rm -f $(SYSTEMD_USER_DIR)/dms.service
|
||||||
|
@echo "Systemd service removed"
|
||||||
|
@echo "Note: Stop/disable service manually if running: systemctl --user stop dms"
|
||||||
|
|
||||||
|
uninstall-icon:
|
||||||
|
@echo "Removing icon..."
|
||||||
|
@rm -f $(ICON_DIR)/danklogo.svg
|
||||||
|
@gtk-update-icon-cache -q $(DATA_DIR)/icons/hicolor 2>/dev/null || true
|
||||||
|
@echo "Icon removed"
|
||||||
|
|
||||||
|
uninstall-desktop:
|
||||||
|
@echo "Removing desktop entry..."
|
||||||
|
@rm -f $(APPLICATIONS_DIR)/dms-open.desktop
|
||||||
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
|
@echo "Desktop entry removed"
|
||||||
|
|
||||||
|
uninstall: uninstall-systemd uninstall-desktop uninstall-icon uninstall-completions uninstall-shell uninstall-bin
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstallation complete!"
|
||||||
|
|
||||||
|
# Target assist
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo ""
|
||||||
|
@echo "Build:"
|
||||||
|
@echo " all (default) - Build the DMS binary"
|
||||||
|
@echo " build - Same as 'all'"
|
||||||
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo ""
|
||||||
|
@echo "Install:"
|
||||||
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
@echo " install-bin - Install only the binary"
|
||||||
|
@echo " install-shell - Install only shell files"
|
||||||
|
@echo " install-completions - Install only shell completions"
|
||||||
|
@echo " install-systemd - Install only systemd service"
|
||||||
|
@echo " install-icon - Install only icon"
|
||||||
|
@echo " install-desktop - Install only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Uninstall:"
|
||||||
|
@echo " uninstall - Remove everything (requires sudo)"
|
||||||
|
@echo " uninstall-bin - Remove only the binary"
|
||||||
|
@echo " uninstall-shell - Remove only shell files"
|
||||||
|
@echo " uninstall-completions - Remove only shell completions"
|
||||||
|
@echo " uninstall-systemd - Remove only systemd service"
|
||||||
|
@echo " uninstall-icon - Remove only icon"
|
||||||
|
@echo " uninstall-desktop - Remove only desktop entry"
|
||||||
|
@echo ""
|
||||||
|
@echo "Usage:"
|
||||||
|
@echo " sudo make install - Build and install DMS"
|
||||||
|
@echo " sudo make uninstall - Remove DMS"
|
||||||
|
@echo " systemctl --user enable --now dms - Enable and start service"
|
||||||
10
assets/dms-open.desktop
Normal file
10
assets/dms-open.desktop
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=DMS Application Picker
|
||||||
|
Comment=Select an application to open links and files
|
||||||
|
Exec=dms open %u
|
||||||
|
Icon=danklogo
|
||||||
|
Terminal=false
|
||||||
|
NoDisplay=true
|
||||||
|
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
|
||||||
|
Categories=Utility;
|
||||||
@@ -5,7 +5,8 @@ After=graphical-session.target
|
|||||||
Requisite=graphical-session.target
|
Requisite=graphical-session.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=dbus
|
||||||
|
BusName=org.freedesktop.Notifications
|
||||||
ExecStart=/usr/bin/dms run --session
|
ExecStart=/usr/bin/dms run --session
|
||||||
ExecReload=/usr/bin/pkill -USR1 -x dms
|
ExecReload=/usr/bin/pkill -USR1 -x dms
|
||||||
Restart=always
|
Restart=always
|
||||||
@@ -1,77 +1,105 @@
|
|||||||
linters-settings:
|
version: "2"
|
||||||
errcheck:
|
|
||||||
check-type-assertions: false
|
|
||||||
check-blank: false
|
|
||||||
exclude-functions:
|
|
||||||
# Cleanup/destroy operations
|
|
||||||
- (io.Closer).Close
|
|
||||||
- (*os.File).Close
|
|
||||||
- (net.Conn).Close
|
|
||||||
- (*net.Conn).Close
|
|
||||||
# Signal handling
|
|
||||||
- (*os.Process).Signal
|
|
||||||
- (*os.Process).Kill
|
|
||||||
# DBus cleanup
|
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
|
||||||
# Encoding to network connections (if conn is bad, nothing we can do)
|
|
||||||
- (*encoding/json.Encoder).Encode
|
|
||||||
- (net.Conn).Write
|
|
||||||
# Command execution where failure is expected/ignored
|
|
||||||
- (*os/exec.Cmd).Run
|
|
||||||
- (*os/exec.Cmd).Start
|
|
||||||
# Flush operations
|
|
||||||
- (*bufio.Writer).Flush
|
|
||||||
# Scanning user input
|
|
||||||
- fmt.Scanln
|
|
||||||
- fmt.Scanf
|
|
||||||
# Parse operations where default value is acceptable
|
|
||||||
- fmt.Sscanf
|
|
||||||
# Flag operations
|
|
||||||
- (*github.com/spf13/pflag.FlagSet).MarkHidden
|
|
||||||
# Binary encoding to buffer (can't fail for basic types)
|
|
||||||
- binary.Write
|
|
||||||
# File operations in cleanup paths
|
|
||||||
- os.Rename
|
|
||||||
- os.Remove
|
|
||||||
- (*os.File).WriteString
|
|
||||||
|
|
||||||
issues:
|
linters:
|
||||||
exclude-rules:
|
enable:
|
||||||
- path: _test\.go
|
- revive
|
||||||
linters:
|
|
||||||
- errcheck
|
settings:
|
||||||
- govet
|
revive:
|
||||||
- unused
|
rules:
|
||||||
- ineffassign
|
- name: use-any
|
||||||
- staticcheck
|
severity: error
|
||||||
- gosimple
|
errcheck:
|
||||||
# Exclude cleanup/teardown method calls from errcheck
|
check-type-assertions: false
|
||||||
- linters:
|
check-blank: false
|
||||||
- errcheck
|
exclude-functions:
|
||||||
text: "Error return value of `.+\\.(Destroy|Release|Stop|Close|Roundtrip|Store)` is not checked"
|
# Cleanup/destroy operations
|
||||||
# Exclude internal state update methods that are best-effort
|
- (io.Closer).Close
|
||||||
- linters:
|
- (*os.File).Close
|
||||||
- errcheck
|
- (net.Conn).Close
|
||||||
text: "Error return value of `[mb]\\.\\w*(update|initialize|recreate|acquire|enumerate|list|List|Ensure|refresh|Lock)\\w*` is not checked"
|
- (*net.Conn).Close
|
||||||
# Exclude SetMode on wayland power controls (best-effort)
|
# Signal handling
|
||||||
- linters:
|
- (*os.Process).Signal
|
||||||
- errcheck
|
- (*os.Process).Kill
|
||||||
text: "Error return value of `.+\\.SetMode` is not checked"
|
# DBus cleanup
|
||||||
# Exclude AddMatchSignal which is best-effort monitoring setup
|
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
||||||
- linters:
|
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
||||||
- errcheck
|
# Encoding to network connections (if conn is bad, nothing we can do)
|
||||||
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
|
- (*encoding/json.Encoder).Encode
|
||||||
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
|
- (net.Conn).Write
|
||||||
- linters:
|
# Command execution where failure is expected/ignored
|
||||||
- errcheck
|
- (*os/exec.Cmd).Run
|
||||||
- ineffassign
|
- (*os/exec.Cmd).Start
|
||||||
path: pkg/go-wayland/
|
# Flush operations
|
||||||
# Exclude proto pkg from ineffassign (generated protocol code)
|
- (*bufio.Writer).Flush
|
||||||
- linters:
|
# Scanning user input
|
||||||
- ineffassign
|
- fmt.Scanln
|
||||||
path: internal/proto/
|
- fmt.Scanf
|
||||||
# binary.Write to bytes.Buffer can't fail
|
# Parse operations where default value is acceptable
|
||||||
- linters:
|
- fmt.Sscanf
|
||||||
- errcheck
|
# Flag operations
|
||||||
text: "Error return value of `binary\\.Write` is not checked"
|
- (*github.com/spf13/pflag.FlagSet).MarkHidden
|
||||||
|
# Binary encoding to buffer (can't fail for basic types)
|
||||||
|
- binary.Write
|
||||||
|
# File operations in cleanup paths
|
||||||
|
- os.Rename
|
||||||
|
- os.Remove
|
||||||
|
- os.RemoveAll
|
||||||
|
- (*os.File).WriteString
|
||||||
|
# Stdout/stderr writes (can't meaningfully handle failure)
|
||||||
|
- fmt.Fprintln
|
||||||
|
- fmt.Fprintf
|
||||||
|
- fmt.Fprint
|
||||||
|
# Writing to pipes (if pipe is bad, nothing we can do)
|
||||||
|
- (*io.PipeWriter).Write
|
||||||
|
- (*os.File).Write
|
||||||
|
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
# Exclude generated mocks from all linters
|
||||||
|
- path: internal/mocks/
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- govet
|
||||||
|
- unused
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- gosimple
|
||||||
|
- revive
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- govet
|
||||||
|
- unused
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- gosimple
|
||||||
|
# Exclude cleanup/teardown method calls from errcheck
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `.+\\.(Destroy|Release|Stop|Close|Roundtrip|Store)` is not checked"
|
||||||
|
# Exclude internal state update methods that are best-effort
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `[mb]\\.\\w*(update|initialize|recreate|acquire|enumerate|list|List|Ensure|refresh|Lock)\\w*` is not checked"
|
||||||
|
# Exclude SetMode on wayland power controls (best-effort)
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `.+\\.SetMode` is not checked"
|
||||||
|
# Exclude AddMatchSignal which is best-effort monitoring setup
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
|
||||||
|
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
- ineffassign
|
||||||
|
path: pkg/go-wayland/
|
||||||
|
# Exclude proto pkg from ineffassign (generated protocol code)
|
||||||
|
- linters:
|
||||||
|
- ineffassign
|
||||||
|
path: internal/proto/
|
||||||
|
# binary.Write to bytes.Buffer can't fail
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `binary\\.Write` is not checked"
|
||||||
|
|||||||
@@ -10,16 +10,19 @@ GO=go
|
|||||||
GOFLAGS=-ldflags="-s -w"
|
GOFLAGS=-ldflags="-s -w"
|
||||||
|
|
||||||
# Version and build info
|
# Version and build info
|
||||||
VERSION=$(shell git describe --tags --always 2>/dev/null || echo "dev")
|
BASE_VERSION=$(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
|
||||||
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
COMMIT_COUNT=$(shell git rev-list --count HEAD 2>/dev/null || echo "0")
|
||||||
COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
COMMIT_HASH=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "unknown")
|
||||||
|
VERSION?=$(BASE_VERSION)+git$(COMMIT_COUNT).$(COMMIT_HASH)
|
||||||
|
BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||||
|
COMMIT?=$(COMMIT_HASH)
|
||||||
|
|
||||||
BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.commit=$(COMMIT)'
|
BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.commit=$(COMMIT)'
|
||||||
|
|
||||||
# Architecture to build for dist target (amd64, arm64, or all)
|
# Architecture to build for dist target (amd64, arm64, or all)
|
||||||
ARCH ?= all
|
ARCH ?= all
|
||||||
|
|
||||||
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps help
|
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
all: build
|
all: build
|
||||||
@@ -132,6 +135,9 @@ version: check-go
|
|||||||
@echo "Build Time: $(BUILD_TIME)"
|
@echo "Build Time: $(BUILD_TIME)"
|
||||||
@echo "Commit: $(COMMIT)"
|
@echo "Commit: $(COMMIT)"
|
||||||
|
|
||||||
|
print-version:
|
||||||
|
@echo "$(VERSION)"
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " all - Build the main binary (dms) (default)"
|
@echo " all - Build the main binary (dms) (default)"
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
|
|
||||||
**Wayland Protocols**
|
**Wayland Protocols**
|
||||||
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
||||||
|
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
|
||||||
|
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
|
||||||
|
- `wp-viewporter` - Fractional scaling support
|
||||||
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
||||||
- `ext-workspace-v1` - Workspace protocol support
|
- `ext-workspace-v1` - Workspace protocol support
|
||||||
- `wlr-output-management-unstable-v1` - Display configuration
|
- `wlr-output-management-unstable-v1` - Display configuration
|
||||||
@@ -44,9 +47,24 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
||||||
- `dms plugins [install|browse|search]` - Plugin management
|
- `dms plugins [install|browse|search]` - Plugin management
|
||||||
- `dms brightness [list|set]` - Control display/monitor brightness
|
- `dms brightness [list|set]` - Control display/monitor brightness
|
||||||
|
- `dms color pick` - Native color picker (see below)
|
||||||
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
||||||
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
||||||
|
|
||||||
|
### Color Picker
|
||||||
|
|
||||||
|
Native Wayland color picker with magnifier, no external dependencies. Supports HiDPI and fractional scaling.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dms color pick # Pick color, output hex
|
||||||
|
dms color pick --rgb # Output as RGB (255 128 64)
|
||||||
|
dms color pick --hsv # Output as HSV (24 75% 100%)
|
||||||
|
dms color pick --json # Output all formats as JSON
|
||||||
|
dms color pick -a # Auto-copy to clipboard
|
||||||
|
```
|
||||||
|
|
||||||
|
The on-screen preview displays the selected format. JSON output includes hex, RGB, HSL, HSV, and CMYK values.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Requires Go 1.24+
|
Requires Go 1.24+
|
||||||
|
|||||||
@@ -28,7 +28,14 @@ var brightnessSetCmd = &cobra.Command{
|
|||||||
Short: "Set brightness for a device",
|
Short: "Set brightness for a device",
|
||||||
Long: "Set brightness percentage (0-100) for a specific device",
|
Long: "Set brightness percentage (0-100) for a specific device",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runBrightnessSet,
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runBrightnessSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
var brightnessGetCmd = &cobra.Command{
|
var brightnessGetCmd = &cobra.Command{
|
||||||
@@ -36,7 +43,14 @@ var brightnessGetCmd = &cobra.Command{
|
|||||||
Short: "Get brightness for a device",
|
Short: "Get brightness for a device",
|
||||||
Long: "Get current brightness percentage for a specific device",
|
Long: "Get current brightness percentage for a specific device",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: runBrightnessGet,
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runBrightnessGet,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -105,9 +119,7 @@ Global Flags:
|
|||||||
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
|
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runBrightnessList(cmd *cobra.Command, args []string) {
|
func getAllBrightnessDevices(includeDDC bool) []brightness.Device {
|
||||||
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
|
||||||
|
|
||||||
allDevices := []brightness.Device{}
|
allDevices := []brightness.Device{}
|
||||||
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
@@ -138,6 +150,13 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return allDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBrightnessList(cmd *cobra.Command, args []string) {
|
||||||
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
allDevices := getAllBrightnessDevices(includeDDC)
|
||||||
|
|
||||||
if len(allDevices) == 0 {
|
if len(allDevices) == 0 {
|
||||||
fmt.Println("No brightness devices found")
|
fmt.Println("No brightness devices found")
|
||||||
return
|
return
|
||||||
@@ -261,31 +280,20 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBrightnessDevices(includeDDC bool) []string {
|
||||||
|
allDevices := getAllBrightnessDevices(includeDDC)
|
||||||
|
|
||||||
|
var deviceIDs []string
|
||||||
|
for _, device := range allDevices {
|
||||||
|
deviceIDs = append(deviceIDs, device.ID)
|
||||||
|
}
|
||||||
|
return deviceIDs
|
||||||
|
}
|
||||||
|
|
||||||
func runBrightnessGet(cmd *cobra.Command, args []string) {
|
func runBrightnessGet(cmd *cobra.Command, args []string) {
|
||||||
deviceID := args[0]
|
deviceID := args[0]
|
||||||
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
includeDDC, _ := cmd.Flags().GetBool("ddc")
|
||||||
|
allDevices := getAllBrightnessDevices(includeDDC)
|
||||||
allDevices := []brightness.Device{}
|
|
||||||
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
|
||||||
if err == nil {
|
|
||||||
devices, err := sysfs.GetDevices()
|
|
||||||
if err == nil {
|
|
||||||
allDevices = append(allDevices, devices...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if includeDDC {
|
|
||||||
ddc, err := brightness.NewDDCBackend()
|
|
||||||
if err == nil {
|
|
||||||
defer ddc.Close()
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
devices, err := ddc.GetDevices()
|
|
||||||
if err == nil {
|
|
||||||
allDevices = append(allDevices, devices...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, device := range allDevices {
|
for _, device := range allDevices {
|
||||||
if device.ID == deviceID {
|
if device.ID == deviceID {
|
||||||
|
|||||||
133
core/cmd/dms/commands_colorpicker.go
Normal file
133
core/cmd/dms/commands_colorpicker.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
colorOutputFmt string
|
||||||
|
colorAutocopy bool
|
||||||
|
colorNotify bool
|
||||||
|
colorLowercase bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var colorCmd = &cobra.Command{
|
||||||
|
Use: "color",
|
||||||
|
Short: "Color utilities",
|
||||||
|
Long: "Color utilities including picking colors from the screen",
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorPickCmd = &cobra.Command{
|
||||||
|
Use: "pick",
|
||||||
|
Short: "Pick a color from the screen",
|
||||||
|
Long: `Pick a color from anywhere on your screen using an interactive color picker.
|
||||||
|
|
||||||
|
Click on any pixel to capture its color, or press Escape to cancel.
|
||||||
|
|
||||||
|
Output format flags (mutually exclusive, default: --hex):
|
||||||
|
--hex - Hexadecimal (#RRGGBB)
|
||||||
|
--rgb - RGB values (R G B)
|
||||||
|
--hsl - HSL values (H S% L%)
|
||||||
|
--hsv - HSV values (H S% V%)
|
||||||
|
--cmyk - CMYK values (C% M% Y% K%)
|
||||||
|
--json - JSON with all formats
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms color pick # Pick color, output as hex
|
||||||
|
dms color pick --rgb # Output as RGB
|
||||||
|
dms color pick --json # Output all formats as JSON
|
||||||
|
dms color pick --hex -l # Output hex in lowercase
|
||||||
|
dms color pick -a # Auto-copy result to clipboard`,
|
||||||
|
Run: runColorPick,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
colorPickCmd.Flags().Bool("hex", false, "Output as hexadecimal (#RRGGBB)")
|
||||||
|
colorPickCmd.Flags().Bool("rgb", false, "Output as RGB (R G B)")
|
||||||
|
colorPickCmd.Flags().Bool("hsl", false, "Output as HSL (H S% L%)")
|
||||||
|
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
||||||
|
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
||||||
|
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
||||||
|
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||||
|
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||||
|
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||||
|
|
||||||
|
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
|
||||||
|
|
||||||
|
colorCmd.AddCommand(colorPickCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runColorPick(cmd *cobra.Command, args []string) {
|
||||||
|
format := colorpicker.FormatHex // default
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
|
if rgb, _ := cmd.Flags().GetBool("rgb"); rgb {
|
||||||
|
format = colorpicker.FormatRGB
|
||||||
|
} else if hsl, _ := cmd.Flags().GetBool("hsl"); hsl {
|
||||||
|
format = colorpicker.FormatHSL
|
||||||
|
} else if hsv, _ := cmd.Flags().GetBool("hsv"); hsv {
|
||||||
|
format = colorpicker.FormatHSV
|
||||||
|
} else if cmyk, _ := cmd.Flags().GetBool("cmyk"); cmyk {
|
||||||
|
format = colorpicker.FormatCMYK
|
||||||
|
}
|
||||||
|
|
||||||
|
config := colorpicker.Config{
|
||||||
|
Format: format,
|
||||||
|
CustomFormat: colorOutputFmt,
|
||||||
|
Lowercase: colorLowercase,
|
||||||
|
Autocopy: colorAutocopy,
|
||||||
|
Notify: colorNotify,
|
||||||
|
}
|
||||||
|
|
||||||
|
picker := colorpicker.New(config)
|
||||||
|
color, err := picker.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if color == nil {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var output string
|
||||||
|
if jsonOutput {
|
||||||
|
jsonStr, err := color.ToJSON()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
output = jsonStr
|
||||||
|
} else {
|
||||||
|
output = color.Format(config.Format, config.Lowercase, config.CustomFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if colorAutocopy {
|
||||||
|
copyToClipboard(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
fmt.Println(output)
|
||||||
|
} else if color.IsDark() {
|
||||||
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyToClipboard(text string) {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if _, err := exec.LookPath("wl-copy"); err == nil {
|
||||||
|
cmd = exec.Command("wl-copy", text)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = cmd.Run()
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -66,6 +68,10 @@ var ipcCmd = &cobra.Command{
|
|||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
||||||
PreRunE: findConfig,
|
PreRunE: findConfig,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
_ = findConfig(cmd, args)
|
||||||
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
runShellIPCCommand(args)
|
runShellIPCCommand(args)
|
||||||
},
|
},
|
||||||
@@ -115,6 +121,12 @@ var pluginsInstallCmd = &cobra.Command{
|
|||||||
Short: "Install a plugin by ID",
|
Short: "Install a plugin by ID",
|
||||||
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getAvailablePluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := installPluginCLI(args[0]); err != nil {
|
if err := installPluginCLI(args[0]); err != nil {
|
||||||
log.Fatalf("Error installing plugin: %v", err)
|
log.Fatalf("Error installing plugin: %v", err)
|
||||||
@@ -127,6 +139,12 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
Short: "Uninstall a plugin by ID",
|
Short: "Uninstall a plugin by ID",
|
||||||
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := uninstallPluginCLI(args[0]); err != nil {
|
if err := uninstallPluginCLI(args[0]); err != nil {
|
||||||
log.Fatalf("Error uninstalling plugin: %v", err)
|
log.Fatalf("Error uninstalling plugin: %v", err)
|
||||||
@@ -136,7 +154,55 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
|
|
||||||
func runVersion(cmd *cobra.Command, args []string) {
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
printASCII()
|
printASCII()
|
||||||
fmt.Printf("%s\n", Version)
|
fmt.Printf("%s\n", formatVersion(Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git builds: dms (git) v0.6.2-XXXX
|
||||||
|
// Stable releases: dms v0.6.2
|
||||||
|
func formatVersion(version string) string {
|
||||||
|
// Arch/Debian/Ubuntu/OpenSUSE git format: 0.6.2+git2264.c5c5ce84
|
||||||
|
re := regexp.MustCompile(`^([\d.]+)\+git(\d+)\.`)
|
||||||
|
if matches := re.FindStringSubmatch(version); matches != nil {
|
||||||
|
return fmt.Sprintf("dms (git) v%s-%s", matches[1], matches[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fedora COPR git format: 0.0.git.2267.d430cae9
|
||||||
|
re = regexp.MustCompile(`^[\d.]+\.git\.(\d+)\.`)
|
||||||
|
if matches := re.FindStringSubmatch(version); matches != nil {
|
||||||
|
baseVersion := getBaseVersion()
|
||||||
|
return fmt.Sprintf("dms (git) v%s-%s", baseVersion, matches[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stable release format: 0.6.2
|
||||||
|
re = regexp.MustCompile(`^([\d.]+)$`)
|
||||||
|
if matches := re.FindStringSubmatch(version); matches != nil {
|
||||||
|
return fmt.Sprintf("dms v%s", matches[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("dms %s", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBaseVersion() string {
|
||||||
|
paths := []string{
|
||||||
|
"/usr/share/quickshell/dms/VERSION",
|
||||||
|
"/usr/local/share/quickshell/dms/VERSION",
|
||||||
|
"/etc/xdg/quickshell/dms/VERSION",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
if content, err := os.ReadFile(path); err == nil {
|
||||||
|
ver := strings.TrimSpace(string(content))
|
||||||
|
ver = strings.TrimPrefix(ver, "v")
|
||||||
|
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
|
||||||
|
if matches := re.FindStringSubmatch(ver); matches != nil {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
return "0.6.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDebugServer() error {
|
func startDebugServer() error {
|
||||||
@@ -299,6 +365,38 @@ func installPluginCLI(idOrName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAvailablePluginIDs() []string {
|
||||||
|
registry, err := plugins.NewRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginList, err := registry.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
for _, p := range pluginList {
|
||||||
|
ids = append(ids, p.ID)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstalledPluginIDs() []string {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
installed, err := manager.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return installed
|
||||||
|
}
|
||||||
|
|
||||||
func uninstallPluginCLI(idOrName string) error {
|
func uninstallPluginCLI(idOrName string) error {
|
||||||
manager, err := plugins.NewManager()
|
manager, err := plugins.NewManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -373,5 +471,8 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
keybindsCmd,
|
keybindsCmd,
|
||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
|
colorCmd,
|
||||||
|
screenshotCmd,
|
||||||
|
notifyActionCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ func init() {
|
|||||||
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||||
dank16Cmd.Flags().String("background", "", "Custom background color")
|
dank16Cmd.Flags().String("background", "", "Custom background color")
|
||||||
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
||||||
|
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDank16(cmd *cobra.Command, args []string) {
|
func runDank16(cmd *cobra.Command, args []string) {
|
||||||
|
|||||||
@@ -16,14 +16,26 @@ var dpmsOnCmd = &cobra.Command{
|
|||||||
Use: "on [output]",
|
Use: "on [output]",
|
||||||
Short: "Turn display(s) on",
|
Short: "Turn display(s) on",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
Run: runDPMSOn,
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runDPMSOn,
|
||||||
}
|
}
|
||||||
|
|
||||||
var dpmsOffCmd = &cobra.Command{
|
var dpmsOffCmd = &cobra.Command{
|
||||||
Use: "off [output]",
|
Use: "off [output]",
|
||||||
Short: "Turn display(s) off",
|
Short: "Turn display(s) off",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
Run: runDPMSOff,
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runDPMSOff,
|
||||||
}
|
}
|
||||||
|
|
||||||
var dpmsListCmd = &cobra.Command{
|
var dpmsListCmd = &cobra.Command{
|
||||||
@@ -71,6 +83,15 @@ func runDPMSOff(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDPMSOutputs() []string {
|
||||||
|
client, err := newDPMSClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
return client.ListOutputs()
|
||||||
|
}
|
||||||
|
|
||||||
func runDPMSList(cmd *cobra.Command, args []string) {
|
func runDPMSList(cmd *cobra.Command, args []string) {
|
||||||
client, err := newDPMSClient()
|
client, err := newDPMSClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -30,14 +30,44 @@ var keybindsShowCmd = &cobra.Command{
|
|||||||
Short: "Show keybinds for a provider",
|
Short: "Show keybinds for a provider",
|
||||||
Long: "Display keybinds/cheatsheet for the specified provider",
|
Long: "Display keybinds/cheatsheet for the specified provider",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: runKeybindsShow,
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
return registry.List(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runKeybindsShow,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsSetCmd = &cobra.Command{
|
||||||
|
Use: "set <provider> <key> <action>",
|
||||||
|
Short: "Set a keybind override",
|
||||||
|
Long: "Create or update a keybind override for the specified provider",
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
Run: runKeybindsSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
var keybindsRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <provider> <key>",
|
||||||
|
Short: "Remove a keybind override",
|
||||||
|
Long: "Remove a keybind override from the specified provider",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: runKeybindsRemove,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
|
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
||||||
|
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
|
||||||
|
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
||||||
|
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
||||||
|
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
||||||
|
|
||||||
keybindsCmd.AddCommand(keybindsListCmd)
|
keybindsCmd.AddCommand(keybindsListCmd)
|
||||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsSetCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
||||||
|
|
||||||
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||||
return providers.NewJSONFileProvider(filePath)
|
return providers.NewJSONFileProvider(filePath)
|
||||||
@@ -64,73 +94,133 @@ func initializeProviders() {
|
|||||||
log.Warnf("Failed to register Sway provider: %v", err)
|
log.Warnf("Failed to register Sway provider: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
niriProvider := providers.NewNiriProvider("")
|
||||||
|
if err := registry.Register(niriProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Niri provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
config := keybinds.DefaultDiscoveryConfig()
|
config := keybinds.DefaultDiscoveryConfig()
|
||||||
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
|
if err := keybinds.AutoDiscoverProviders(registry, config); err != nil {
|
||||||
log.Warnf("Failed to auto-discover providers: %v", err)
|
log.Warnf("Failed to auto-discover providers: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeybindsList(cmd *cobra.Command, args []string) {
|
func runKeybindsList(_ *cobra.Command, _ []string) {
|
||||||
registry := keybinds.GetDefaultRegistry()
|
providerList := keybinds.GetDefaultRegistry().List()
|
||||||
providers := registry.List()
|
if len(providerList) == 0 {
|
||||||
|
|
||||||
if len(providers) == 0 {
|
|
||||||
fmt.Fprintln(os.Stdout, "No providers available")
|
fmt.Fprintln(os.Stdout, "No providers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(os.Stdout, "Available providers:")
|
fmt.Fprintln(os.Stdout, "Available providers:")
|
||||||
for _, name := range providers {
|
for _, name := range providerList {
|
||||||
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeybindsShow(cmd *cobra.Command, args []string) {
|
func makeProviderWithPath(name, path string) keybinds.Provider {
|
||||||
providerName := args[0]
|
switch name {
|
||||||
registry := keybinds.GetDefaultRegistry()
|
case "hyprland":
|
||||||
|
return providers.NewHyprlandProvider(path)
|
||||||
customPath, _ := cmd.Flags().GetString("path")
|
case "mangowc":
|
||||||
if customPath != "" {
|
return providers.NewMangoWCProvider(path)
|
||||||
var provider keybinds.Provider
|
case "sway":
|
||||||
switch providerName {
|
return providers.NewSwayProvider(path)
|
||||||
case "hyprland":
|
case "niri":
|
||||||
provider = providers.NewHyprlandProvider(customPath)
|
return providers.NewNiriProvider(path)
|
||||||
case "mangowc":
|
default:
|
||||||
provider = providers.NewMangoWCProvider(customPath)
|
return nil
|
||||||
case "sway":
|
|
||||||
provider = providers.NewSwayProvider(customPath)
|
|
||||||
default:
|
|
||||||
log.Fatalf("Provider %s does not support custom path", providerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
sheet, err := provider.GetCheatSheet()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting cheatsheet: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := json.MarshalIndent(sheet, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error generating JSON: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
provider, err := registry.Get(providerName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error: %v", err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCheatSheet(provider keybinds.Provider) {
|
||||||
sheet, err := provider.GetCheatSheet()
|
sheet, err := provider.GetCheatSheet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error getting cheatsheet: %v", err)
|
log.Fatalf("Error getting cheatsheet: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := json.MarshalIndent(sheet, "", " ")
|
output, err := json.MarshalIndent(sheet, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error generating JSON: %v", err)
|
log.Fatalf("Error generating JSON: %v", err)
|
||||||
}
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsShow(cmd *cobra.Command, args []string) {
|
||||||
|
providerName := args[0]
|
||||||
|
customPath, _ := cmd.Flags().GetString("path")
|
||||||
|
|
||||||
|
if customPath != "" {
|
||||||
|
provider := makeProviderWithPath(providerName, customPath)
|
||||||
|
if provider == nil {
|
||||||
|
log.Fatalf("Provider %s does not support custom path", providerName)
|
||||||
|
}
|
||||||
|
printCheatSheet(provider)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := keybinds.GetDefaultRegistry().Get(providerName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
printCheatSheet(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWritableProvider(name string) keybinds.WritableProvider {
|
||||||
|
provider, err := keybinds.GetDefaultRegistry().Get(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
writable, ok := provider.(keybinds.WritableProvider)
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("Provider %s does not support writing keybinds", name)
|
||||||
|
}
|
||||||
|
return writable
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsSet(cmd *cobra.Command, args []string) {
|
||||||
|
providerName, key, action := args[0], args[1], args[2]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||||
|
_ = writable.RemoveBind(replaceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := make(map[string]any)
|
||||||
|
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||||
|
options["allow-when-locked"] = true
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||||
|
options["cooldown-ms"] = v
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||||
|
options["repeat"] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
desc, _ := cmd.Flags().GetString("desc")
|
||||||
|
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||||
|
log.Fatalf("Error setting keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"action": action,
|
||||||
|
"path": writable.GetOverridePath(),
|
||||||
|
}, "", " ")
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsRemove(_ *cobra.Command, args []string) {
|
||||||
|
providerName, key := args[0], args[1]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if err := writable.RemoveBind(key); err != nil {
|
||||||
|
log.Fatalf("Error removing keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"removed": true,
|
||||||
|
}, "", " ")
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
}
|
}
|
||||||
|
|||||||
227
core/cmd/dms/commands_open.go
Normal file
227
core/cmd/dms/commands_open.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
openMimeType string
|
||||||
|
openCategories []string
|
||||||
|
openRequestType string
|
||||||
|
)
|
||||||
|
|
||||||
|
var openCmd = &cobra.Command{
|
||||||
|
Use: "open [target]",
|
||||||
|
Short: "Open a file, URL, or resource with an application picker",
|
||||||
|
Long: `Open a target (URL, file, or other resource) using the DMS application picker.
|
||||||
|
By default, this opens URLs with the browser picker. You can customize the behavior
|
||||||
|
with flags to handle different MIME types or application categories.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms open https://example.com # Open URL with browser picker
|
||||||
|
dms open file.pdf --mime application/pdf # Open PDF with compatible apps
|
||||||
|
dms open document.odt --category Office # Open with office applications
|
||||||
|
dms open --mime image/png image.png # Open image with image viewers`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
runOpen(args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(openCmd)
|
||||||
|
openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications")
|
||||||
|
openCmd.Flags().StringSliceVar(&openCategories, "category", []string{}, "Application categories to filter (e.g., WebBrowser, Office, Graphics)")
|
||||||
|
openCmd.Flags().StringVar(&openRequestType, "type", "url", "Request type (url, file, or custom)")
|
||||||
|
_ = openCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return []string{"url", "file", "custom"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// mimeTypeToCategories maps MIME types to desktop file categories
|
||||||
|
func mimeTypeToCategories(mimeType string) []string {
|
||||||
|
// Split MIME type to get the main type
|
||||||
|
parts := strings.Split(mimeType, "/")
|
||||||
|
if len(parts) < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mainType := parts[0]
|
||||||
|
|
||||||
|
switch mainType {
|
||||||
|
case "image":
|
||||||
|
return []string{"Graphics", "Viewer"}
|
||||||
|
case "video":
|
||||||
|
return []string{"Video", "AudioVideo"}
|
||||||
|
case "audio":
|
||||||
|
return []string{"Audio", "AudioVideo"}
|
||||||
|
case "text":
|
||||||
|
if strings.Contains(mimeType, "html") {
|
||||||
|
return []string{"WebBrowser"}
|
||||||
|
}
|
||||||
|
return []string{"TextEditor", "Office"}
|
||||||
|
case "application":
|
||||||
|
if strings.Contains(mimeType, "pdf") {
|
||||||
|
return []string{"Office", "Viewer"}
|
||||||
|
}
|
||||||
|
if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "spreadsheet") ||
|
||||||
|
strings.Contains(mimeType, "presentation") || strings.Contains(mimeType, "msword") ||
|
||||||
|
strings.Contains(mimeType, "ms-excel") || strings.Contains(mimeType, "ms-powerpoint") ||
|
||||||
|
strings.Contains(mimeType, "opendocument") {
|
||||||
|
return []string{"Office"}
|
||||||
|
}
|
||||||
|
if strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") ||
|
||||||
|
strings.Contains(mimeType, "gzip") || strings.Contains(mimeType, "compress") {
|
||||||
|
return []string{"Archiving", "Utility"}
|
||||||
|
}
|
||||||
|
return []string{"Office", "Viewer"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runOpen(target string) {
|
||||||
|
socketPath, err := server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket not found: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("DMS socket connection failed: %v", err)
|
||||||
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
for {
|
||||||
|
_, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if buf[0] == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse file:// URIs to extract the actual file path
|
||||||
|
actualTarget := target
|
||||||
|
detectedMimeType := openMimeType
|
||||||
|
detectedCategories := openCategories
|
||||||
|
detectedRequestType := openRequestType
|
||||||
|
|
||||||
|
log.Infof("Processing target: %s", target)
|
||||||
|
|
||||||
|
if parsedURL, err := url.Parse(target); err == nil && parsedURL.Scheme == "file" {
|
||||||
|
// Extract file path from file:// URI and convert to absolute path
|
||||||
|
actualTarget = parsedURL.Path
|
||||||
|
if absPath, err := filepath.Abs(actualTarget); err == nil {
|
||||||
|
actualTarget = absPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedRequestType == "url" || detectedRequestType == "" {
|
||||||
|
detectedRequestType = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Detected file:// URI, extracted absolute path: %s", actualTarget)
|
||||||
|
|
||||||
|
// Auto-detect MIME type if not provided
|
||||||
|
if detectedMimeType == "" {
|
||||||
|
ext := filepath.Ext(actualTarget)
|
||||||
|
if ext != "" {
|
||||||
|
detectedMimeType = mime.TypeByExtension(ext)
|
||||||
|
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect categories based on MIME type if not provided
|
||||||
|
if len(detectedCategories) == 0 && detectedMimeType != "" {
|
||||||
|
detectedCategories = mimeTypeToCategories(detectedMimeType)
|
||||||
|
log.Infof("Detected categories from MIME type: %v", detectedCategories)
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
|
||||||
|
// Handle HTTP(S) URLs
|
||||||
|
if detectedRequestType == "" {
|
||||||
|
detectedRequestType = "url"
|
||||||
|
}
|
||||||
|
log.Infof("Detected HTTP(S) URL")
|
||||||
|
} else if _, err := os.Stat(target); err == nil {
|
||||||
|
// Handle local file paths directly (not file:// URIs)
|
||||||
|
// Convert to absolute path
|
||||||
|
if absPath, err := filepath.Abs(target); err == nil {
|
||||||
|
actualTarget = absPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedRequestType == "url" || detectedRequestType == "" {
|
||||||
|
detectedRequestType = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Detected local file path, converted to absolute: %s", actualTarget)
|
||||||
|
|
||||||
|
// Auto-detect MIME type if not provided
|
||||||
|
if detectedMimeType == "" {
|
||||||
|
ext := filepath.Ext(actualTarget)
|
||||||
|
if ext != "" {
|
||||||
|
detectedMimeType = mime.TypeByExtension(ext)
|
||||||
|
log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-detect categories based on MIME type if not provided
|
||||||
|
if len(detectedCategories) == 0 && detectedMimeType != "" {
|
||||||
|
detectedCategories = mimeTypeToCategories(detectedMimeType)
|
||||||
|
log.Infof("Detected categories from MIME type: %v", detectedCategories)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[string]any{
|
||||||
|
"target": actualTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedMimeType != "" {
|
||||||
|
params["mimeType"] = detectedMimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(detectedCategories) > 0 {
|
||||||
|
params["categories"] = detectedCategories
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectedRequestType != "" {
|
||||||
|
params["requestType"] = detectedRequestType
|
||||||
|
}
|
||||||
|
|
||||||
|
method := "apppicker.open"
|
||||||
|
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
|
||||||
|
method = "browser.open"
|
||||||
|
params["url"] = target
|
||||||
|
}
|
||||||
|
|
||||||
|
req := models.Request{
|
||||||
|
ID: 1,
|
||||||
|
Method: method,
|
||||||
|
Params: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
||||||
|
|
||||||
|
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
||||||
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Request sent successfully")
|
||||||
|
}
|
||||||
377
core/cmd/dms/commands_screenshot.go
Normal file
377
core/cmd/dms/commands_screenshot.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ssOutputName string
|
||||||
|
ssIncludeCursor bool
|
||||||
|
ssFormat string
|
||||||
|
ssQuality int
|
||||||
|
ssOutputDir string
|
||||||
|
ssFilename string
|
||||||
|
ssNoClipboard bool
|
||||||
|
ssNoFile bool
|
||||||
|
ssNoNotify bool
|
||||||
|
ssStdout bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var screenshotCmd = &cobra.Command{
|
||||||
|
Use: "screenshot",
|
||||||
|
Short: "Capture screenshots",
|
||||||
|
Long: `Capture screenshots from Wayland displays.
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
region - Select a region interactively (default)
|
||||||
|
full - Capture the focused output
|
||||||
|
all - Capture all outputs combined
|
||||||
|
output - Capture a specific output by name
|
||||||
|
window - Capture the focused window (Hyprland only)
|
||||||
|
last - Capture the last selected region
|
||||||
|
|
||||||
|
Output format (--format):
|
||||||
|
png - PNG format (default)
|
||||||
|
jpg/jpeg - JPEG format
|
||||||
|
ppm - PPM format
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms screenshot # Region select, save file + clipboard
|
||||||
|
dms screenshot full # Full screen of focused output
|
||||||
|
dms screenshot all # All screens combined
|
||||||
|
dms screenshot output -o DP-1 # Specific output
|
||||||
|
dms screenshot window # Focused window (Hyprland)
|
||||||
|
dms screenshot last # Last region (pre-selected)
|
||||||
|
dms screenshot --no-clipboard # Save file only
|
||||||
|
dms screenshot --no-file # Clipboard only
|
||||||
|
dms screenshot --cursor # Include cursor
|
||||||
|
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssRegionCmd = &cobra.Command{
|
||||||
|
Use: "region",
|
||||||
|
Short: "Select a region interactively",
|
||||||
|
Run: runScreenshotRegion,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssFullCmd = &cobra.Command{
|
||||||
|
Use: "full",
|
||||||
|
Short: "Capture the focused output",
|
||||||
|
Run: runScreenshotFull,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssAllCmd = &cobra.Command{
|
||||||
|
Use: "all",
|
||||||
|
Short: "Capture all outputs combined",
|
||||||
|
Run: runScreenshotAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssOutputCmd = &cobra.Command{
|
||||||
|
Use: "output",
|
||||||
|
Short: "Capture a specific output",
|
||||||
|
Run: runScreenshotOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssLastCmd = &cobra.Command{
|
||||||
|
Use: "last",
|
||||||
|
Short: "Capture the last selected region",
|
||||||
|
Long: `Capture the previously selected region without interactive selection.
|
||||||
|
If no previous region exists, falls back to interactive selection.`,
|
||||||
|
Run: runScreenshotLast,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssWindowCmd = &cobra.Command{
|
||||||
|
Use: "window",
|
||||||
|
Short: "Capture the focused window",
|
||||||
|
Long: `Capture the currently focused window.
|
||||||
|
Currently only supported on Hyprland.`,
|
||||||
|
Run: runScreenshotWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List available outputs",
|
||||||
|
Run: runScreenshotList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var notifyActionCmd = &cobra.Command{
|
||||||
|
Use: "notify-action",
|
||||||
|
Hidden: true,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
screenshot.RunNotifyActionListener(args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||||
|
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||||
|
screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
|
||||||
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
|
screenshotCmd.AddCommand(ssFullCmd)
|
||||||
|
screenshotCmd.AddCommand(ssAllCmd)
|
||||||
|
screenshotCmd.AddCommand(ssOutputCmd)
|
||||||
|
screenshotCmd.AddCommand(ssLastCmd)
|
||||||
|
screenshotCmd.AddCommand(ssWindowCmd)
|
||||||
|
screenshotCmd.AddCommand(ssListCmd)
|
||||||
|
|
||||||
|
screenshotCmd.Run = runScreenshotRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||||
|
config := screenshot.DefaultConfig()
|
||||||
|
config.Mode = mode
|
||||||
|
config.OutputName = ssOutputName
|
||||||
|
config.IncludeCursor = ssIncludeCursor
|
||||||
|
config.Clipboard = !ssNoClipboard
|
||||||
|
config.SaveFile = !ssNoFile
|
||||||
|
config.Notify = !ssNoNotify
|
||||||
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
|
if ssOutputDir != "" {
|
||||||
|
config.OutputDir = ssOutputDir
|
||||||
|
}
|
||||||
|
if ssFilename != "" {
|
||||||
|
config.Filename = ssFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(ssFormat) {
|
||||||
|
case "jpg", "jpeg":
|
||||||
|
config.Format = screenshot.FormatJPEG
|
||||||
|
case "ppm":
|
||||||
|
config.Format = screenshot.FormatPPM
|
||||||
|
default:
|
||||||
|
config.Format = screenshot.FormatPNG
|
||||||
|
}
|
||||||
|
|
||||||
|
if ssQuality < 1 {
|
||||||
|
ssQuality = 1
|
||||||
|
}
|
||||||
|
if ssQuality > 100 {
|
||||||
|
ssQuality = 100
|
||||||
|
}
|
||||||
|
config.Quality = ssQuality
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshot(config screenshot.Config) {
|
||||||
|
sc := screenshot.New(config)
|
||||||
|
result, err := sc.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer result.Buffer.Close()
|
||||||
|
|
||||||
|
if result.YInverted {
|
||||||
|
result.Buffer.FlipVertical()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Stdout {
|
||||||
|
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath string
|
||||||
|
|
||||||
|
if config.SaveFile {
|
||||||
|
outputDir := config.OutputDir
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = screenshot.GetOutputDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := config.Filename
|
||||||
|
if filename == "" {
|
||||||
|
filename = screenshot.GenerateFilename(config.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = filepath.Join(outputDir, filename)
|
||||||
|
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Clipboard {
|
||||||
|
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !config.SaveFile {
|
||||||
|
fmt.Println("Copied to clipboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Notify {
|
||||||
|
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
||||||
|
screenshot.SendNotification(screenshot.NotifyResult{
|
||||||
|
FilePath: filePath,
|
||||||
|
Clipboard: config.Clipboard,
|
||||||
|
ImageData: thumbData,
|
||||||
|
Width: thumbW,
|
||||||
|
Height: thumbH,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
var mimeType string
|
||||||
|
var data bytes.Buffer
|
||||||
|
|
||||||
|
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
mimeType = "image/png"
|
||||||
|
if err := screenshot.EncodePNG(&data, img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("wl-copy", "--type", mimeType)
|
||||||
|
cmd.Stdin = &data
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
return screenshot.EncodeJPEG(os.Stdout, img, quality)
|
||||||
|
default:
|
||||||
|
return screenshot.EncodePNG(os.Stdout, img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
|
||||||
|
srcW, srcH := buf.Width, buf.Height
|
||||||
|
scale := 1.0
|
||||||
|
if srcW > maxSize || srcH > maxSize {
|
||||||
|
if srcW > srcH {
|
||||||
|
scale = float64(maxSize) / float64(srcW)
|
||||||
|
} else {
|
||||||
|
scale = float64(maxSize) / float64(srcH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dstW := int(float64(srcW) * scale)
|
||||||
|
dstH := int(float64(srcH) * scale)
|
||||||
|
if dstW < 1 {
|
||||||
|
dstW = 1
|
||||||
|
}
|
||||||
|
if dstH < 1 {
|
||||||
|
dstH = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf.Data()
|
||||||
|
rgb := make([]byte, dstW*dstH*3)
|
||||||
|
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
|
||||||
|
|
||||||
|
for y := 0; y < dstH; y++ {
|
||||||
|
srcY := int(float64(y) / scale)
|
||||||
|
if srcY >= srcH {
|
||||||
|
srcY = srcH - 1
|
||||||
|
}
|
||||||
|
for x := 0; x < dstW; x++ {
|
||||||
|
srcX := int(float64(x) / scale)
|
||||||
|
if srcX >= srcW {
|
||||||
|
srcX = srcW - 1
|
||||||
|
}
|
||||||
|
si := srcY*buf.Stride + srcX*4
|
||||||
|
di := (y*dstW + x) * 3
|
||||||
|
if si+2 < len(data) {
|
||||||
|
if swapRB {
|
||||||
|
rgb[di+0] = data[si+2]
|
||||||
|
rgb[di+1] = data[si+1]
|
||||||
|
rgb[di+2] = data[si+0]
|
||||||
|
} else {
|
||||||
|
rgb[di+0] = data[si+0]
|
||||||
|
rgb[di+1] = data[si+1]
|
||||||
|
rgb[di+2] = data[si+2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgb, dstW, dstH
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotRegion(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeRegion)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotAll(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeAllScreens)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotOutput(cmd *cobra.Command, args []string) {
|
||||||
|
if ssOutputName == "" && len(args) > 0 {
|
||||||
|
ssOutputName = args[0]
|
||||||
|
}
|
||||||
|
if ssOutputName == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
config := getScreenshotConfig(screenshot.ModeOutput)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotLast(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeLastRegion)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotWindow(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeWindow)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotList(cmd *cobra.Command, args []string) {
|
||||||
|
outputs, err := screenshot.ListOutputs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range outputs {
|
||||||
|
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
|
||||||
|
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ipcTargets map[string][]string
|
||||||
|
|
||||||
var isSessionManaged bool
|
var isSessionManaged bool
|
||||||
|
|
||||||
func execDetachedRestart(targetPID int) {
|
func execDetachedRestart(targetPID int) {
|
||||||
@@ -68,7 +70,7 @@ func getPIDFilePath() string {
|
|||||||
|
|
||||||
func writePIDFile(childPID int) error {
|
func writePIDFile(childPID int) error {
|
||||||
pidFile := getPIDFilePath()
|
pidFile := getPIDFilePath()
|
||||||
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0644)
|
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removePIDFile() {
|
func removePIDFile() {
|
||||||
@@ -144,7 +146,7 @@ func runShellInteractive(session bool) {
|
|||||||
socketPath := server.GetSocketPath()
|
socketPath := server.GetSocketPath()
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
|
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
log.Warnf("Failed to write config state file: %v", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(configStateFile)
|
defer os.Remove(configStateFile)
|
||||||
@@ -370,7 +372,7 @@ func runShellDaemon(session bool) {
|
|||||||
socketPath := server.GetSocketPath()
|
socketPath := server.GetSocketPath()
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0644); err != nil {
|
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
log.Warnf("Failed to write config state file: %v", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(configStateFile)
|
defer os.Remove(configStateFile)
|
||||||
@@ -473,6 +475,51 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||||
|
targets := map[string][]string{}
|
||||||
|
var currentTarget string
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
if strings.HasPrefix(line, "target ") {
|
||||||
|
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
||||||
|
currentFunc := strings.TrimPrefix(line, " function ")
|
||||||
|
currentFunc = strings.SplitN(currentFunc, "(", 2)[0]
|
||||||
|
targets[currentTarget] = append(targets[currentTarget], currentFunc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
func getShellIPCCompletions(args []string, toComplete string) []string {
|
||||||
|
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
||||||
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
var targets ipcTargets
|
||||||
|
|
||||||
|
if output, err := cmd.Output(); err == nil {
|
||||||
|
log.Debugf("IPC show output: %s", string(output))
|
||||||
|
targets = parseTargetsFromIPCShowOutput(string(output))
|
||||||
|
} else {
|
||||||
|
log.Debugf("Error getting IPC show output for completions: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 && args[0] == "call" {
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
targetNames := make([]string, 0)
|
||||||
|
targetNames = append(targetNames, "call")
|
||||||
|
for k := range targets {
|
||||||
|
targetNames = append(targetNames, k)
|
||||||
|
}
|
||||||
|
return targetNames
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets[args[0]]
|
||||||
|
}
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
log.Error("IPC command requires arguments")
|
log.Error("IPC command requires arguments")
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/godbus/dbus/v5 v5.2.0
|
github.com/godbus/dbus/v5 v5.2.0
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||||
|
github.com/pilebones/go-udev v0.9.1
|
||||||
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
||||||
|
|||||||
22
core/go.sum
22
core/go.sum
@@ -24,16 +24,12 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
|
|||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
|
|
||||||
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
|
||||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
@@ -43,8 +39,6 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
|
|||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -62,19 +56,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0 h1:EC9n6hr6yKDoVJ6g7Ko523LbbceJfR0ohbOp809Fyf4=
|
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251111123000-fb5ff8f3f0b0/go.mod h1:E3VhlS+AKkrq6ZNn1axE2/nDRJ87l1FJk9r5HT2vPX0=
|
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9 h1:SOFrnF9LCssC6q6Rb0084Bzg2aBYbe8QXv9xKGXmt/w=
|
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251112161705-8cc3e21f07a9/go.mod h1:0wtvm/JfPC9RFVEAP3ks0ec5h64/qmZkTTUE3pjz7Hc=
|
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
@@ -95,8 +84,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -111,6 +101,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
||||||
|
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -120,6 +112,8 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
|||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
||||||
|
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
@@ -137,12 +131,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
|
|||||||
306
core/internal/colorpicker/color.go
Normal file
306
core/internal/colorpicker/color.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Color struct {
|
||||||
|
R, G, B, A uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputFormat int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatHex OutputFormat = iota
|
||||||
|
FormatRGB
|
||||||
|
FormatHSL
|
||||||
|
FormatHSV
|
||||||
|
FormatCMYK
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseFormat(s string) OutputFormat {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "rgb":
|
||||||
|
return FormatRGB
|
||||||
|
case "hsl":
|
||||||
|
return FormatHSL
|
||||||
|
case "hsv":
|
||||||
|
return FormatHSV
|
||||||
|
case "cmyk":
|
||||||
|
return FormatCMYK
|
||||||
|
default:
|
||||||
|
return FormatHex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) ToHex(lowercase bool) string {
|
||||||
|
if lowercase {
|
||||||
|
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) ToRGB() string {
|
||||||
|
return fmt.Sprintf("%d %d %d", c.R, c.G, c.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) ToHSL() string {
|
||||||
|
h, s, l := rgbToHSL(c.R, c.G, c.B)
|
||||||
|
return fmt.Sprintf("%d %d%% %d%%", h, s, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) ToHSV() string {
|
||||||
|
h, s, v := rgbToHSV(c.R, c.G, c.B)
|
||||||
|
return fmt.Sprintf("%d %d%% %d%%", h, s, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) ToCMYK() string {
|
||||||
|
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
|
||||||
|
return fmt.Sprintf("%d%% %d%% %d%% %d%%", cy, m, y, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) Format(format OutputFormat, lowercase bool, customFmt string) string {
|
||||||
|
if customFmt != "" {
|
||||||
|
return c.formatCustom(format, customFmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case FormatRGB:
|
||||||
|
return c.ToRGB()
|
||||||
|
case FormatHSL:
|
||||||
|
return c.ToHSL()
|
||||||
|
case FormatHSV:
|
||||||
|
return c.ToHSV()
|
||||||
|
case FormatCMYK:
|
||||||
|
return c.ToCMYK()
|
||||||
|
default:
|
||||||
|
return c.ToHex(lowercase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) formatCustom(format OutputFormat, customFmt string) string {
|
||||||
|
switch format {
|
||||||
|
case FormatRGB:
|
||||||
|
return replaceArgs(customFmt, c.R, c.G, c.B)
|
||||||
|
case FormatHSL:
|
||||||
|
h, s, l := rgbToHSL(c.R, c.G, c.B)
|
||||||
|
return replaceArgs(customFmt, h, s, l)
|
||||||
|
case FormatHSV:
|
||||||
|
h, s, v := rgbToHSV(c.R, c.G, c.B)
|
||||||
|
return replaceArgs(customFmt, h, s, v)
|
||||||
|
case FormatCMYK:
|
||||||
|
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
|
||||||
|
return replaceArgs4(customFmt, cy, m, y, k)
|
||||||
|
default:
|
||||||
|
if strings.Contains(customFmt, "{0}") {
|
||||||
|
r := fmt.Sprintf("%02X", c.R)
|
||||||
|
g := fmt.Sprintf("%02X", c.G)
|
||||||
|
b := fmt.Sprintf("%02X", c.B)
|
||||||
|
return replaceArgsStr(customFmt, r, g, b)
|
||||||
|
}
|
||||||
|
return c.ToHex(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceArgs[T any](format string, a, b, c T) string {
|
||||||
|
result := format
|
||||||
|
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
|
||||||
|
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
|
||||||
|
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceArgs4[T any](format string, a, b, c, d T) string {
|
||||||
|
result := format
|
||||||
|
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
|
||||||
|
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
|
||||||
|
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
|
||||||
|
result = strings.ReplaceAll(result, "{3}", fmt.Sprintf("%v", d))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceArgsStr(format, a, b, c string) string {
|
||||||
|
result := format
|
||||||
|
result = strings.ReplaceAll(result, "{0}", a)
|
||||||
|
result = strings.ReplaceAll(result, "{1}", b)
|
||||||
|
result = strings.ReplaceAll(result, "{2}", c)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func rgbToHSL(r, g, b uint8) (int, int, int) {
|
||||||
|
rf := float64(r) / 255.0
|
||||||
|
gf := float64(g) / 255.0
|
||||||
|
bf := float64(b) / 255.0
|
||||||
|
|
||||||
|
maxVal := math.Max(rf, math.Max(gf, bf))
|
||||||
|
minVal := math.Min(rf, math.Min(gf, bf))
|
||||||
|
l := (maxVal + minVal) / 2
|
||||||
|
|
||||||
|
if maxVal == minVal {
|
||||||
|
return 0, 0, int(math.Round(l * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
d := maxVal - minVal
|
||||||
|
var s float64
|
||||||
|
if l > 0.5 {
|
||||||
|
s = d / (2 - maxVal - minVal)
|
||||||
|
} else {
|
||||||
|
s = d / (maxVal + minVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
var h float64
|
||||||
|
switch maxVal {
|
||||||
|
case rf:
|
||||||
|
h = (gf - bf) / d
|
||||||
|
if gf < bf {
|
||||||
|
h += 6
|
||||||
|
}
|
||||||
|
case gf:
|
||||||
|
h = (bf-rf)/d + 2
|
||||||
|
case bf:
|
||||||
|
h = (rf-gf)/d + 4
|
||||||
|
}
|
||||||
|
h /= 6
|
||||||
|
|
||||||
|
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(l * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
func rgbToHSV(r, g, b uint8) (int, int, int) {
|
||||||
|
rf := float64(r) / 255.0
|
||||||
|
gf := float64(g) / 255.0
|
||||||
|
bf := float64(b) / 255.0
|
||||||
|
|
||||||
|
maxVal := math.Max(rf, math.Max(gf, bf))
|
||||||
|
minVal := math.Min(rf, math.Min(gf, bf))
|
||||||
|
v := maxVal
|
||||||
|
d := maxVal - minVal
|
||||||
|
|
||||||
|
var s float64
|
||||||
|
if maxVal != 0 {
|
||||||
|
s = d / maxVal
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxVal == minVal {
|
||||||
|
return 0, int(math.Round(s * 100)), int(math.Round(v * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
var h float64
|
||||||
|
switch maxVal {
|
||||||
|
case rf:
|
||||||
|
h = (gf - bf) / d
|
||||||
|
if gf < bf {
|
||||||
|
h += 6
|
||||||
|
}
|
||||||
|
case gf:
|
||||||
|
h = (bf-rf)/d + 2
|
||||||
|
case bf:
|
||||||
|
h = (rf-gf)/d + 4
|
||||||
|
}
|
||||||
|
h /= 6
|
||||||
|
|
||||||
|
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(v * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
func rgbToCMYK(r, g, b uint8) (int, int, int, int) {
|
||||||
|
if r == 0 && g == 0 && b == 0 {
|
||||||
|
return 0, 0, 0, 100
|
||||||
|
}
|
||||||
|
|
||||||
|
rf := float64(r) / 255.0
|
||||||
|
gf := float64(g) / 255.0
|
||||||
|
bf := float64(b) / 255.0
|
||||||
|
|
||||||
|
k := 1 - math.Max(rf, math.Max(gf, bf))
|
||||||
|
c := (1 - rf - k) / (1 - k)
|
||||||
|
m := (1 - gf - k) / (1 - k)
|
||||||
|
y := (1 - bf - k) / (1 - k)
|
||||||
|
|
||||||
|
return int(math.Round(c * 100)), int(math.Round(m * 100)), int(math.Round(y * 100)), int(math.Round(k * 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) Luminance() float64 {
|
||||||
|
r := float64(c.R) / 255.0
|
||||||
|
g := float64(c.G) / 255.0
|
||||||
|
b := float64(c.B) / 255.0
|
||||||
|
|
||||||
|
if r <= 0.03928 {
|
||||||
|
r = r / 12.92
|
||||||
|
} else {
|
||||||
|
r = math.Pow((r+0.055)/1.055, 2.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g <= 0.03928 {
|
||||||
|
g = g / 12.92
|
||||||
|
} else {
|
||||||
|
g = math.Pow((g+0.055)/1.055, 2.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b <= 0.03928 {
|
||||||
|
b = b / 12.92
|
||||||
|
} else {
|
||||||
|
b = math.Pow((b+0.055)/1.055, 2.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.2126*r + 0.7152*g + 0.0722*b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) IsDark() bool {
|
||||||
|
return c.Luminance() < 0.179
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColorJSON struct {
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
RGB struct {
|
||||||
|
R int `json:"r"`
|
||||||
|
G int `json:"g"`
|
||||||
|
B int `json:"b"`
|
||||||
|
} `json:"rgb"`
|
||||||
|
HSL struct {
|
||||||
|
H int `json:"h"`
|
||||||
|
S int `json:"s"`
|
||||||
|
L int `json:"l"`
|
||||||
|
} `json:"hsl"`
|
||||||
|
HSV struct {
|
||||||
|
H int `json:"h"`
|
||||||
|
S int `json:"s"`
|
||||||
|
V int `json:"v"`
|
||||||
|
} `json:"hsv"`
|
||||||
|
CMYK struct {
|
||||||
|
C int `json:"c"`
|
||||||
|
M int `json:"m"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
K int `json:"k"`
|
||||||
|
} `json:"cmyk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) ToJSON() (string, error) {
|
||||||
|
h, s, l := rgbToHSL(c.R, c.G, c.B)
|
||||||
|
hv, sv, v := rgbToHSV(c.R, c.G, c.B)
|
||||||
|
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
|
||||||
|
|
||||||
|
data := ColorJSON{
|
||||||
|
Hex: c.ToHex(false),
|
||||||
|
}
|
||||||
|
data.RGB.R = int(c.R)
|
||||||
|
data.RGB.G = int(c.G)
|
||||||
|
data.RGB.B = int(c.B)
|
||||||
|
data.HSL.H = h
|
||||||
|
data.HSL.S = s
|
||||||
|
data.HSL.L = l
|
||||||
|
data.HSV.H = hv
|
||||||
|
data.HSV.S = sv
|
||||||
|
data.HSV.V = v
|
||||||
|
data.CMYK.C = cy
|
||||||
|
data.CMYK.M = m
|
||||||
|
data.CMYK.Y = y
|
||||||
|
data.CMYK.K = k
|
||||||
|
|
||||||
|
bytes, err := json.MarshalIndent(data, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
730
core/internal/colorpicker/picker.go
Normal file
730
core/internal/colorpicker/picker.go
Normal file
@@ -0,0 +1,730 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Format OutputFormat
|
||||||
|
CustomFormat string
|
||||||
|
Lowercase bool
|
||||||
|
Autocopy bool
|
||||||
|
Notify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Output struct {
|
||||||
|
wlOutput *client.Output
|
||||||
|
name string
|
||||||
|
globalName uint32
|
||||||
|
x, y int32
|
||||||
|
width int32
|
||||||
|
height int32
|
||||||
|
scale int32
|
||||||
|
fractionalScale float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayerSurface struct {
|
||||||
|
output *Output
|
||||||
|
state *SurfaceState
|
||||||
|
wlSurface *client.Surface
|
||||||
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
|
viewport *wp_viewporter.WpViewport
|
||||||
|
wlPool *client.ShmPool
|
||||||
|
wlBuffer *client.Buffer
|
||||||
|
bufferBusy bool
|
||||||
|
oldPool *client.ShmPool
|
||||||
|
oldBuffer *client.Buffer
|
||||||
|
scopyBuffer *client.Buffer
|
||||||
|
configured bool
|
||||||
|
hidden bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Picker struct {
|
||||||
|
config Config
|
||||||
|
|
||||||
|
display *client.Display
|
||||||
|
registry *client.Registry
|
||||||
|
ctx *client.Context
|
||||||
|
|
||||||
|
compositor *client.Compositor
|
||||||
|
shm *client.Shm
|
||||||
|
seat *client.Seat
|
||||||
|
pointer *client.Pointer
|
||||||
|
keyboard *client.Keyboard
|
||||||
|
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||||
|
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||||
|
viewporter *wp_viewporter.WpViewporter
|
||||||
|
|
||||||
|
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
||||||
|
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
||||||
|
|
||||||
|
outputs map[uint32]*Output
|
||||||
|
outputsMu sync.Mutex
|
||||||
|
|
||||||
|
surfaces []*LayerSurface
|
||||||
|
activeSurface *LayerSurface
|
||||||
|
|
||||||
|
running bool
|
||||||
|
pickedColor *Color
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Picker {
|
||||||
|
return &Picker{
|
||||||
|
config: config,
|
||||||
|
outputs: make(map[uint32]*Output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) Run() (*Color, error) {
|
||||||
|
if err := p.connect(); err != nil {
|
||||||
|
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer p.cleanup()
|
||||||
|
|
||||||
|
if err := p.setupRegistry(); err != nil {
|
||||||
|
return nil, fmt.Errorf("registry setup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.screencopy == nil {
|
||||||
|
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.layerShell == nil {
|
||||||
|
return nil, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.seat == nil {
|
||||||
|
return nil, fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.createSurfaces(); err != nil {
|
||||||
|
return nil, fmt.Errorf("create surfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.running = true
|
||||||
|
for p.running {
|
||||||
|
if err := p.ctx.Dispatch(); err != nil {
|
||||||
|
p.err = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
p.checkDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.err != nil {
|
||||||
|
return nil, p.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.pickedColor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) checkDone() {
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
picked, cancelled := ls.state.IsDone()
|
||||||
|
switch {
|
||||||
|
case cancelled:
|
||||||
|
p.running = false
|
||||||
|
return
|
||||||
|
case picked:
|
||||||
|
color, ok := ls.state.PickColor()
|
||||||
|
if ok {
|
||||||
|
p.pickedColor = &color
|
||||||
|
}
|
||||||
|
p.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) connect() error {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.display = display
|
||||||
|
p.ctx = display.Context()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) roundtrip() error {
|
||||||
|
return wlhelpers.Roundtrip(p.display, p.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupRegistry() error {
|
||||||
|
registry, err := p.display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.registry = registry
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
p.handleGlobal(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
delete(p.outputs, e.Name)
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case client.CompositorInterfaceName:
|
||||||
|
compositor := client.NewCompositor(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
|
||||||
|
p.compositor = compositor
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.ShmInterfaceName:
|
||||||
|
shm := client.NewShm(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||||
|
p.shm = shm
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.SeatInterfaceName:
|
||||||
|
seat := client.NewSeat(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
|
||||||
|
p.seat = seat
|
||||||
|
p.setupInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
output := client.NewOutput(p.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
p.outputs[e.Name] = &Output{
|
||||||
|
wlOutput: output,
|
||||||
|
globalName: e.Name,
|
||||||
|
scale: 1,
|
||||||
|
fractionalScale: 1.0,
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
p.setupOutputHandlers(e.Name, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||||
|
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
||||||
|
p.layerShell = layerShell
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
|
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 3 {
|
||||||
|
version = 3
|
||||||
|
}
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
||||||
|
p.screencopy = screencopy
|
||||||
|
}
|
||||||
|
|
||||||
|
case wp_viewporter.WpViewporterInterfaceName:
|
||||||
|
viewporter := wp_viewporter.NewWpViewporter(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, viewporter); err == nil {
|
||||||
|
p.viewporter = viewporter
|
||||||
|
}
|
||||||
|
|
||||||
|
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
||||||
|
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
p.shortcutsInhibitMgr = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
|
||||||
|
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.x = e.X
|
||||||
|
o.y = e.Y
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||||
|
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.width = e.Width
|
||||||
|
o.height = e.Height
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.scale = e.Factor
|
||||||
|
o.fractionalScale = float64(e.Factor)
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.name = e.Name
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) createSurfaces() error {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
outputs := make([]*Output, 0, len(p.outputs))
|
||||||
|
for _, o := range p.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
|
||||||
|
for _, output := range outputs {
|
||||||
|
ls, err := p.createLayerSurface(output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("output %s: %w", output.name, err)
|
||||||
|
}
|
||||||
|
p.surfaces = append(p.surfaces, ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
|
||||||
|
surface, err := p.compositor.CreateSurface()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create surface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerSurf, err := p.layerShell.GetLayerSurface(
|
||||||
|
surface,
|
||||||
|
output.wlOutput,
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
|
||||||
|
"dms-colorpicker",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get layer surface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := &LayerSurface{
|
||||||
|
output: output,
|
||||||
|
state: NewSurfaceState(p.config.Format, p.config.Lowercase),
|
||||||
|
wlSurface: surface,
|
||||||
|
layerSurf: layerSurf,
|
||||||
|
hidden: true, // Start hidden, will show overlay when pointer enters
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.viewporter != nil {
|
||||||
|
vp, err := p.viewporter.GetViewport(surface)
|
||||||
|
if err == nil {
|
||||||
|
ls.viewport = vp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := layerSurf.SetAnchor(
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
|
||||||
|
); err != nil {
|
||||||
|
log.Warn("failed to set layer anchor", "err", err)
|
||||||
|
}
|
||||||
|
if err := layerSurf.SetExclusiveZone(-1); err != nil {
|
||||||
|
log.Warn("failed to set exclusive zone", "err", err)
|
||||||
|
}
|
||||||
|
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
|
||||||
|
log.Warn("failed to set keyboard interactivity", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
|
||||||
|
if err := layerSurf.AckConfigure(e.Serial); err != nil {
|
||||||
|
log.Warn("failed to ack configure", "err", err)
|
||||||
|
}
|
||||||
|
if err := ls.state.OnLayerConfigure(int(e.Width), int(e.Height)); err != nil {
|
||||||
|
log.Warn("failed to handle layer configure", "err", err)
|
||||||
|
}
|
||||||
|
ls.configured = true
|
||||||
|
|
||||||
|
scale := p.computeSurfaceScale(ls)
|
||||||
|
ls.state.SetScale(scale)
|
||||||
|
|
||||||
|
if !ls.state.IsReady() {
|
||||||
|
p.captureForSurface(ls)
|
||||||
|
} else {
|
||||||
|
p.redrawSurface(ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request shortcut inhibition once surface is configured
|
||||||
|
p.ensureShortcutsInhibitor(ls)
|
||||||
|
})
|
||||||
|
|
||||||
|
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
|
||||||
|
p.running = false
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := surface.Commit(); err != nil {
|
||||||
|
log.Warn("failed to commit surface", "err", err)
|
||||||
|
}
|
||||||
|
return ls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
|
||||||
|
out := ls.output
|
||||||
|
if out == nil || out.fractionalScale <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := int32(math.Ceil(out.fractionalScale))
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
return scale
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
|
||||||
|
if p.shortcutsInhibitMgr == nil || p.seat == nil || p.shortcutsInhibitor != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inhibitor, err := p.shortcutsInhibitMgr.InhibitShortcuts(ls.wlSurface, p.seat)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("failed to create shortcuts inhibitor", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.shortcutsInhibitor = inhibitor
|
||||||
|
|
||||||
|
inhibitor.SetActiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1ActiveEvent) {
|
||||||
|
log.Debug("shortcuts inhibitor active")
|
||||||
|
})
|
||||||
|
|
||||||
|
inhibitor.SetInactiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1InactiveEvent) {
|
||||||
|
log.Debug("shortcuts inhibitor deactivated by compositor")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) captureForSurface(ls *LayerSurface) {
|
||||||
|
frame, err := p.screencopy.CaptureOutput(0, ls.output.wlOutput)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
if err := ls.state.OnScreencopyBuffer(PixelFormat(e.Format), int(e.Width), int(e.Height), int(e.Stride)); err != nil {
|
||||||
|
log.Error("failed to create screencopy buffer", "err", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
|
||||||
|
screenBuf := ls.state.ScreenBuffer()
|
||||||
|
if screenBuf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := p.shm.CreatePool(screenBuf.Fd(), int32(screenBuf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wlBuffer, err := pool.CreateBuffer(0, int32(screenBuf.Width), int32(screenBuf.Height), int32(screenBuf.Stride), uint32(ls.state.screenFormat))
|
||||||
|
if err != nil {
|
||||||
|
pool.Destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.scopyBuffer != nil {
|
||||||
|
ls.scopyBuffer.Destroy()
|
||||||
|
}
|
||||||
|
ls.scopyBuffer = wlBuffer
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
|
||||||
|
|
||||||
|
if err := frame.Copy(wlBuffer); err != nil {
|
||||||
|
log.Error("failed to copy frame", "err", err)
|
||||||
|
}
|
||||||
|
pool.Destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
|
||||||
|
ls.state.OnScreencopyFlags(e.Flags)
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||||
|
ls.state.OnScreencopyReady()
|
||||||
|
scale := p.computeSurfaceScale(ls)
|
||||||
|
ls.state.SetScale(scale)
|
||||||
|
frame.Destroy()
|
||||||
|
p.redrawSurface(ls)
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
|
||||||
|
frame.Destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||||
|
var renderBuf *ShmBuffer
|
||||||
|
if ls.hidden {
|
||||||
|
renderBuf = ls.state.RedrawScreenOnly()
|
||||||
|
} else {
|
||||||
|
renderBuf = ls.state.Redraw()
|
||||||
|
}
|
||||||
|
if renderBuf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.oldBuffer != nil {
|
||||||
|
ls.oldBuffer.Destroy()
|
||||||
|
ls.oldBuffer = nil
|
||||||
|
}
|
||||||
|
if ls.oldPool != nil {
|
||||||
|
ls.oldPool.Destroy()
|
||||||
|
ls.oldPool = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.oldPool = ls.wlPool
|
||||||
|
ls.oldBuffer = ls.wlBuffer
|
||||||
|
ls.wlPool = nil
|
||||||
|
ls.wlBuffer = nil
|
||||||
|
|
||||||
|
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.wlPool = pool
|
||||||
|
|
||||||
|
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.wlBuffer = wlBuffer
|
||||||
|
|
||||||
|
lsRef := ls
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
lsRef.bufferBusy = false
|
||||||
|
})
|
||||||
|
ls.bufferBusy = true
|
||||||
|
|
||||||
|
logicalW, logicalH := ls.state.LogicalSize()
|
||||||
|
if logicalW == 0 || logicalH == 0 {
|
||||||
|
logicalW = int(ls.output.width)
|
||||||
|
logicalH = int(ls.output.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := ls.state.Scale()
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.viewport != nil {
|
||||||
|
srcW := float64(renderBuf.Width) / float64(scale)
|
||||||
|
srcH := float64(renderBuf.Height) / float64(scale)
|
||||||
|
_ = ls.viewport.SetSource(0, 0, srcW, srcH)
|
||||||
|
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
|
||||||
|
}
|
||||||
|
_ = ls.wlSurface.SetBufferScale(scale)
|
||||||
|
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||||
|
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||||
|
_ = ls.wlSurface.Commit()
|
||||||
|
|
||||||
|
ls.state.SwapBuffers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) hideSurface(ls *LayerSurface) {
|
||||||
|
if ls == nil || ls.wlSurface == nil || ls.hidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.hidden = true
|
||||||
|
// Redraw without the crosshair overlay
|
||||||
|
p.redrawSurface(ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupInput() {
|
||||||
|
if p.seat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
|
||||||
|
pointer, err := p.seat.GetPointer()
|
||||||
|
if err == nil {
|
||||||
|
p.pointer = pointer
|
||||||
|
p.setupPointerHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
|
||||||
|
keyboard, err := p.seat.GetKeyboard()
|
||||||
|
if err == nil {
|
||||||
|
p.keyboard = keyboard
|
||||||
|
p.setupKeyboardHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupPointerHandlers() {
|
||||||
|
p.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
|
||||||
|
if err := p.pointer.SetCursor(e.Serial, nil, 0, 0); err != nil {
|
||||||
|
log.Debug("failed to hide cursor", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.activeSurface = nil
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
|
p.activeSurface = ls
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.activeSurface.hidden {
|
||||||
|
p.activeSurface.hidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
|
p.redrawSurface(p.activeSurface)
|
||||||
|
})
|
||||||
|
|
||||||
|
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
|
p.hideSurface(ls)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
p.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
|
||||||
|
if p.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
|
p.redrawSurface(p.activeSurface)
|
||||||
|
})
|
||||||
|
|
||||||
|
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||||
|
if p.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.activeSurface.state.OnPointerButton(e.Button, e.State)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupKeyboardHandlers() {
|
||||||
|
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
ls.state.OnKey(e.Key, e.State)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) cleanup() {
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
if ls.scopyBuffer != nil {
|
||||||
|
ls.scopyBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.oldBuffer != nil {
|
||||||
|
ls.oldBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.oldPool != nil {
|
||||||
|
ls.oldPool.Destroy()
|
||||||
|
}
|
||||||
|
if ls.wlBuffer != nil {
|
||||||
|
ls.wlBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.wlPool != nil {
|
||||||
|
ls.wlPool.Destroy()
|
||||||
|
}
|
||||||
|
if ls.viewport != nil {
|
||||||
|
ls.viewport.Destroy()
|
||||||
|
}
|
||||||
|
if ls.layerSurf != nil {
|
||||||
|
ls.layerSurf.Destroy()
|
||||||
|
}
|
||||||
|
if ls.wlSurface != nil {
|
||||||
|
ls.wlSurface.Destroy()
|
||||||
|
}
|
||||||
|
if ls.state != nil {
|
||||||
|
ls.state.Destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.shortcutsInhibitor != nil {
|
||||||
|
if err := p.shortcutsInhibitor.Destroy(); err != nil {
|
||||||
|
log.Debug("failed to destroy shortcuts inhibitor", "err", err)
|
||||||
|
}
|
||||||
|
p.shortcutsInhibitor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.shortcutsInhibitMgr != nil {
|
||||||
|
if err := p.shortcutsInhibitMgr.Destroy(); err != nil {
|
||||||
|
log.Debug("failed to destroy shortcuts inhibit manager", "err", err)
|
||||||
|
}
|
||||||
|
p.shortcutsInhibitMgr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.viewporter != nil {
|
||||||
|
p.viewporter.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.screencopy != nil {
|
||||||
|
p.screencopy.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.pointer != nil {
|
||||||
|
p.pointer.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.keyboard != nil {
|
||||||
|
p.keyboard.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.display != nil {
|
||||||
|
p.ctx.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
40
core/internal/colorpicker/shm.go
Normal file
40
core/internal/colorpicker/shm.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
|
|
||||||
|
type ShmBuffer = shm.Buffer
|
||||||
|
|
||||||
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
|
return shm.CreateBuffer(width, height, stride)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
|
||||||
|
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPixelColorWithFormat(buf *ShmBuffer, x, y int, format PixelFormat) Color {
|
||||||
|
if x < 0 || x >= buf.Width || y < 0 || y >= buf.Height {
|
||||||
|
return Color{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf.Data()
|
||||||
|
offset := y*buf.Stride + x*4
|
||||||
|
if offset+3 >= len(data) {
|
||||||
|
return Color{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == FormatABGR8888 || format == FormatXBGR8888 {
|
||||||
|
return Color{
|
||||||
|
R: data[offset],
|
||||||
|
G: data[offset+1],
|
||||||
|
B: data[offset+2],
|
||||||
|
A: data[offset+3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Color{
|
||||||
|
B: data[offset],
|
||||||
|
G: data[offset+1],
|
||||||
|
R: data[offset+2],
|
||||||
|
A: data[offset+3],
|
||||||
|
}
|
||||||
|
}
|
||||||
1119
core/internal/colorpicker/state.go
Normal file
1119
core/internal/colorpicker/state.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -110,7 +110,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deployNiriConfig handles Niri configuration deployment with backup and merging
|
|
||||||
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Niri",
|
ConfigType: "Niri",
|
||||||
@@ -123,6 +122,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
var existingConfig string
|
var existingConfig string
|
||||||
if _, err := os.Stat(result.Path); err == nil {
|
if _, err := os.Stat(result.Path); err == nil {
|
||||||
cd.log("Found existing Niri configuration")
|
cd.log("Found existing Niri configuration")
|
||||||
@@ -143,14 +148,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect polkit agent path
|
|
||||||
polkitPath, err := cd.detectPolkitAgent()
|
polkitPath, err := cd.detectPolkitAgent()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine terminal command based on choice
|
|
||||||
var terminalCommand string
|
var terminalCommand string
|
||||||
switch terminal {
|
switch terminal {
|
||||||
case deps.TerminalGhostty:
|
case deps.TerminalGhostty:
|
||||||
@@ -160,13 +163,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
case deps.TerminalAlacritty:
|
case deps.TerminalAlacritty:
|
||||||
terminalCommand = "alacritty"
|
terminalCommand = "alacritty"
|
||||||
default:
|
default:
|
||||||
terminalCommand = "ghostty" // fallback to ghostty
|
terminalCommand = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
|
||||||
// If there was an existing config, merge the output sections
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,11 +184,38 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := cd.deployNiriDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
result.Deployed = true
|
result.Deployed = true
|
||||||
cd.log("Successfully deployed Niri configuration")
|
cd.log("Successfully deployed Niri configuration")
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) error {
|
||||||
|
configs := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
}{
|
||||||
|
{"colors.kdl", NiriColorsConfig},
|
||||||
|
{"layout.kdl", NiriLayoutConfig},
|
||||||
|
{"alttab.kdl", NiriAlttabConfig},
|
||||||
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
|
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
|||||||
@@ -479,9 +479,10 @@ general {
|
|||||||
func TestNiriConfigStructure(t *testing.T) {
|
func TestNiriConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, NiriConfig, "input {")
|
assert.Contains(t, NiriConfig, "input {")
|
||||||
assert.Contains(t, NiriConfig, "layout {")
|
assert.Contains(t, NiriConfig, "layout {")
|
||||||
assert.Contains(t, NiriConfig, "binds {")
|
|
||||||
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
||||||
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
|
||||||
|
assert.Contains(t, NiriBindsConfig, "binds {")
|
||||||
|
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandConfigStructure(t *testing.T) {
|
func TestHyprlandConfigStructure(t *testing.T) {
|
||||||
|
|||||||
@@ -22,8 +22,19 @@ func LocateDMSConfig() (string, error) {
|
|||||||
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryPaths = append(primaryPaths, "/usr/share/quickshell/dms")
|
// System data directories
|
||||||
|
dataDirs := os.Getenv("XDG_DATA_DIRS")
|
||||||
|
if dataDirs == "" {
|
||||||
|
dataDirs = "/usr/local/share:/usr/share"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range strings.Split(dataDirs, ":") {
|
||||||
|
if dir != "" {
|
||||||
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System config directories (fallback)
|
||||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||||
if configDirs == "" {
|
if configDirs == "" {
|
||||||
configDirs = "/etc/xdg"
|
configDirs = "/etc/xdg"
|
||||||
|
|||||||
@@ -140,8 +140,8 @@ $mod = SUPER
|
|||||||
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
||||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
bind = $mod, space, exec, dms ipc call spotlight toggle
|
||||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
bind = $mod, V, exec, dms ipc call clipboard toggle
|
||||||
bind = $mod, M, exec, dms ipc call processlist toggle
|
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
|
||||||
bind = $mod, comma, exec, dms ipc call settings toggle
|
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
|
||||||
bind = $mod, N, exec, dms ipc call notifications toggle
|
bind = $mod, N, exec, dms ipc call notifications toggle
|
||||||
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
||||||
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
||||||
@@ -153,7 +153,7 @@ bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
|||||||
# === Security ===
|
# === Security ===
|
||||||
bind = $mod ALT, L, exec, dms ipc call lock lock
|
bind = $mod ALT, L, exec, dms ipc call lock lock
|
||||||
bind = $mod SHIFT, E, exit
|
bind = $mod SHIFT, E, exit
|
||||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist toggle
|
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
||||||
|
|
||||||
# === Audio Controls ===
|
# === Audio Controls ===
|
||||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
||||||
|
|||||||
5
core/internal/config/embedded/niri-alttab.kdl
Normal file
5
core/internal/config/embedded/niri-alttab.kdl
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
recent-windows {
|
||||||
|
highlight {
|
||||||
|
corner-radius 12
|
||||||
|
}
|
||||||
|
}
|
||||||
195
core/internal/config/embedded/niri-binds.kdl
Normal file
195
core/internal/config/embedded/niri-binds.kdl
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
binds {
|
||||||
|
// === System & Overview ===
|
||||||
|
Mod+D repeat=false { toggle-overview; }
|
||||||
|
Mod+Tab repeat=false { toggle-overview; }
|
||||||
|
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||||
|
|
||||||
|
// === Application Launchers ===
|
||||||
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
|
||||||
|
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||||
|
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||||
|
}
|
||||||
|
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
||||||
|
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
||||||
|
}
|
||||||
|
Mod+M hotkey-overlay-title="Task Manager" {
|
||||||
|
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||||
|
}
|
||||||
|
Mod+Comma hotkey-overlay-title="Settings" {
|
||||||
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
|
}
|
||||||
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
|
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
||||||
|
}
|
||||||
|
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||||
|
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
|
|
||||||
|
// === Security ===
|
||||||
|
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
|
||||||
|
spawn "dms" "ipc" "call" "lock" "lock";
|
||||||
|
}
|
||||||
|
Mod+Shift+E { quit; }
|
||||||
|
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
||||||
|
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Audio Controls ===
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
||||||
|
}
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
||||||
|
}
|
||||||
|
XF86AudioMute allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "mute";
|
||||||
|
}
|
||||||
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Brightness Controls ===
|
||||||
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
||||||
|
}
|
||||||
|
XF86MonBrightnessDown allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Window Management ===
|
||||||
|
Mod+Q repeat=false { close-window; }
|
||||||
|
Mod+F { maximize-column; }
|
||||||
|
Mod+Shift+F { fullscreen-window; }
|
||||||
|
Mod+Shift+T { toggle-window-floating; }
|
||||||
|
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||||
|
Mod+W { toggle-column-tabbed-display; }
|
||||||
|
|
||||||
|
// === Focus Navigation ===
|
||||||
|
Mod+Left { focus-column-left; }
|
||||||
|
Mod+Down { focus-window-down; }
|
||||||
|
Mod+Up { focus-window-up; }
|
||||||
|
Mod+Right { focus-column-right; }
|
||||||
|
Mod+H { focus-column-left; }
|
||||||
|
Mod+J { focus-window-down; }
|
||||||
|
Mod+K { focus-window-up; }
|
||||||
|
Mod+L { focus-column-right; }
|
||||||
|
|
||||||
|
// === Window Movement ===
|
||||||
|
Mod+Shift+Left { move-column-left; }
|
||||||
|
Mod+Shift+Down { move-window-down; }
|
||||||
|
Mod+Shift+Up { move-window-up; }
|
||||||
|
Mod+Shift+Right { move-column-right; }
|
||||||
|
Mod+Shift+H { move-column-left; }
|
||||||
|
Mod+Shift+J { move-window-down; }
|
||||||
|
Mod+Shift+K { move-window-up; }
|
||||||
|
Mod+Shift+L { move-column-right; }
|
||||||
|
|
||||||
|
// === Column Navigation ===
|
||||||
|
Mod+Home { focus-column-first; }
|
||||||
|
Mod+End { focus-column-last; }
|
||||||
|
Mod+Ctrl+Home { move-column-to-first; }
|
||||||
|
Mod+Ctrl+End { move-column-to-last; }
|
||||||
|
|
||||||
|
// === Monitor Navigation ===
|
||||||
|
Mod+Ctrl+Left { focus-monitor-left; }
|
||||||
|
//Mod+Ctrl+Down { focus-monitor-down; }
|
||||||
|
//Mod+Ctrl+Up { focus-monitor-up; }
|
||||||
|
Mod+Ctrl+Right { focus-monitor-right; }
|
||||||
|
Mod+Ctrl+H { focus-monitor-left; }
|
||||||
|
Mod+Ctrl+J { focus-monitor-down; }
|
||||||
|
Mod+Ctrl+K { focus-monitor-up; }
|
||||||
|
Mod+Ctrl+L { focus-monitor-right; }
|
||||||
|
|
||||||
|
// === Move to Monitor ===
|
||||||
|
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||||
|
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||||
|
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||||
|
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||||
|
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
|
||||||
|
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
||||||
|
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
||||||
|
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
|
||||||
|
|
||||||
|
// === Workspace Navigation ===
|
||||||
|
Mod+Page_Down { focus-workspace-down; }
|
||||||
|
Mod+Page_Up { focus-workspace-up; }
|
||||||
|
Mod+U { focus-workspace-down; }
|
||||||
|
Mod+I { focus-workspace-up; }
|
||||||
|
Mod+Ctrl+Down { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+Up { move-column-to-workspace-up; }
|
||||||
|
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
// === Move Workspaces ===
|
||||||
|
Mod+Shift+Page_Down { move-workspace-down; }
|
||||||
|
Mod+Shift+Page_Up { move-workspace-up; }
|
||||||
|
Mod+Shift+U { move-workspace-down; }
|
||||||
|
Mod+Shift+I { move-workspace-up; }
|
||||||
|
|
||||||
|
// === Mouse Wheel Navigation ===
|
||||||
|
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||||
|
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||||
|
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||||
|
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
Mod+WheelScrollRight { focus-column-right; }
|
||||||
|
Mod+WheelScrollLeft { focus-column-left; }
|
||||||
|
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
||||||
|
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
||||||
|
|
||||||
|
Mod+Shift+WheelScrollDown { focus-column-right; }
|
||||||
|
Mod+Shift+WheelScrollUp { focus-column-left; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
||||||
|
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
||||||
|
|
||||||
|
// === Numbered Workspaces ===
|
||||||
|
Mod+1 { focus-workspace 1; }
|
||||||
|
Mod+2 { focus-workspace 2; }
|
||||||
|
Mod+3 { focus-workspace 3; }
|
||||||
|
Mod+4 { focus-workspace 4; }
|
||||||
|
Mod+5 { focus-workspace 5; }
|
||||||
|
Mod+6 { focus-workspace 6; }
|
||||||
|
Mod+7 { focus-workspace 7; }
|
||||||
|
Mod+8 { focus-workspace 8; }
|
||||||
|
Mod+9 { focus-workspace 9; }
|
||||||
|
|
||||||
|
// === Move to Numbered Workspaces ===
|
||||||
|
Mod+Shift+1 { move-column-to-workspace 1; }
|
||||||
|
Mod+Shift+2 { move-column-to-workspace 2; }
|
||||||
|
Mod+Shift+3 { move-column-to-workspace 3; }
|
||||||
|
Mod+Shift+4 { move-column-to-workspace 4; }
|
||||||
|
Mod+Shift+5 { move-column-to-workspace 5; }
|
||||||
|
Mod+Shift+6 { move-column-to-workspace 6; }
|
||||||
|
Mod+Shift+7 { move-column-to-workspace 7; }
|
||||||
|
Mod+Shift+8 { move-column-to-workspace 8; }
|
||||||
|
Mod+Shift+9 { move-column-to-workspace 9; }
|
||||||
|
|
||||||
|
// === Column Management ===
|
||||||
|
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||||
|
Mod+BracketRight { consume-or-expel-window-right; }
|
||||||
|
Mod+Period { expel-window-from-column; }
|
||||||
|
|
||||||
|
// === Sizing & Layout ===
|
||||||
|
Mod+R { switch-preset-column-width; }
|
||||||
|
Mod+Shift+R { switch-preset-window-height; }
|
||||||
|
Mod+Ctrl+R { reset-window-height; }
|
||||||
|
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||||
|
Mod+C { center-column; }
|
||||||
|
Mod+Ctrl+C { center-visible-columns; }
|
||||||
|
|
||||||
|
// === Manual Sizing ===
|
||||||
|
Mod+Minus { set-column-width "-10%"; }
|
||||||
|
Mod+Equal { set-column-width "+10%"; }
|
||||||
|
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||||
|
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||||
|
|
||||||
|
// === Screenshots ===
|
||||||
|
XF86Launch1 { screenshot; }
|
||||||
|
Ctrl+XF86Launch1 { screenshot-screen; }
|
||||||
|
Alt+XF86Launch1 { screenshot-window; }
|
||||||
|
Print { screenshot; }
|
||||||
|
Ctrl+Print { screenshot-screen; }
|
||||||
|
Alt+Print { screenshot-window; }
|
||||||
|
// === System Controls ===
|
||||||
|
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||||
|
Mod+Shift+P { power-off-monitors; }
|
||||||
|
}
|
||||||
36
core/internal/config/embedded/niri-colors.kdl
Normal file
36
core/internal/config/embedded/niri-colors.kdl
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
layout {
|
||||||
|
background-color "transparent"
|
||||||
|
|
||||||
|
focus-ring {
|
||||||
|
active-color "#9dcbfb"
|
||||||
|
inactive-color "#8c9199"
|
||||||
|
urgent-color "#ffb4ab"
|
||||||
|
}
|
||||||
|
|
||||||
|
border {
|
||||||
|
active-color "#9dcbfb"
|
||||||
|
inactive-color "#8c9199"
|
||||||
|
urgent-color "#ffb4ab"
|
||||||
|
}
|
||||||
|
|
||||||
|
shadow {
|
||||||
|
color "#00000070"
|
||||||
|
}
|
||||||
|
|
||||||
|
tab-indicator {
|
||||||
|
active-color "#9dcbfb"
|
||||||
|
inactive-color "#8c9199"
|
||||||
|
urgent-color "#ffb4ab"
|
||||||
|
}
|
||||||
|
|
||||||
|
insert-hint {
|
||||||
|
color "#9dcbfb80"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recent-windows {
|
||||||
|
highlight {
|
||||||
|
active-color "#124a73"
|
||||||
|
urgent-color "#ffb4ab"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
core/internal/config/embedded/niri-layout.kdl
Normal file
17
core/internal/config/embedded/niri-layout.kdl
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
layout {
|
||||||
|
gaps 4
|
||||||
|
|
||||||
|
border {
|
||||||
|
width 2
|
||||||
|
}
|
||||||
|
|
||||||
|
focus-ring {
|
||||||
|
width 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window-rule {
|
||||||
|
geometry-corner-radius 12
|
||||||
|
clip-to-geometry true
|
||||||
|
tiled-state true
|
||||||
|
draw-border-with-background false
|
||||||
|
}
|
||||||
@@ -214,210 +214,27 @@ window-rule {
|
|||||||
match app-id="zoom"
|
match app-id="zoom"
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
window-rule {
|
|
||||||
geometry-corner-radius 12
|
|
||||||
clip-to-geometry true
|
|
||||||
}
|
|
||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
binds {
|
|
||||||
// === System & Overview ===
|
|
||||||
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
|
|
||||||
Mod+Tab repeat=false { toggle-overview; }
|
|
||||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
|
||||||
|
|
||||||
// === Application Launchers ===
|
|
||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
|
|
||||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
|
||||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
|
||||||
}
|
|
||||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
|
||||||
}
|
|
||||||
Mod+M hotkey-overlay-title="Task Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
|
||||||
}
|
|
||||||
Mod+Comma hotkey-overlay-title="Settings" {
|
|
||||||
spawn "dms" "ipc" "call" "settings" "toggle";
|
|
||||||
}
|
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
|
||||||
}
|
|
||||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
|
||||||
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
|
||||||
|
|
||||||
// === Security ===
|
|
||||||
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
|
|
||||||
spawn "dms" "ipc" "call" "lock" "lock";
|
|
||||||
}
|
|
||||||
Mod+Shift+E { quit; }
|
|
||||||
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Audio Controls ===
|
|
||||||
XF86AudioRaiseVolume allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
|
||||||
}
|
|
||||||
XF86AudioLowerVolume allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
|
||||||
}
|
|
||||||
XF86AudioMute allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "mute";
|
|
||||||
}
|
|
||||||
XF86AudioMicMute allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Brightness Controls ===
|
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
|
||||||
}
|
|
||||||
XF86MonBrightnessDown allow-when-locked=true {
|
|
||||||
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Window Management ===
|
|
||||||
Mod+Q repeat=false { close-window; }
|
|
||||||
Mod+F { maximize-column; }
|
|
||||||
Mod+Shift+F { fullscreen-window; }
|
|
||||||
Mod+Shift+T { toggle-window-floating; }
|
|
||||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
|
||||||
Mod+W { toggle-column-tabbed-display; }
|
|
||||||
|
|
||||||
// === Focus Navigation ===
|
|
||||||
Mod+Left { focus-column-left; }
|
|
||||||
Mod+Down { focus-window-down; }
|
|
||||||
Mod+Up { focus-window-up; }
|
|
||||||
Mod+Right { focus-column-right; }
|
|
||||||
Mod+H { focus-column-left; }
|
|
||||||
Mod+J { focus-window-down; }
|
|
||||||
Mod+K { focus-window-up; }
|
|
||||||
Mod+L { focus-column-right; }
|
|
||||||
|
|
||||||
// === Window Movement ===
|
|
||||||
Mod+Shift+Left { move-column-left; }
|
|
||||||
Mod+Shift+Down { move-window-down; }
|
|
||||||
Mod+Shift+Up { move-window-up; }
|
|
||||||
Mod+Shift+Right { move-column-right; }
|
|
||||||
Mod+Shift+H { move-column-left; }
|
|
||||||
Mod+Shift+J { move-window-down; }
|
|
||||||
Mod+Shift+K { move-window-up; }
|
|
||||||
Mod+Shift+L { move-column-right; }
|
|
||||||
|
|
||||||
// === Column Navigation ===
|
|
||||||
Mod+Home { focus-column-first; }
|
|
||||||
Mod+End { focus-column-last; }
|
|
||||||
Mod+Ctrl+Home { move-column-to-first; }
|
|
||||||
Mod+Ctrl+End { move-column-to-last; }
|
|
||||||
|
|
||||||
// === Monitor Navigation ===
|
|
||||||
Mod+Ctrl+Left { focus-monitor-left; }
|
|
||||||
//Mod+Ctrl+Down { focus-monitor-down; }
|
|
||||||
//Mod+Ctrl+Up { focus-monitor-up; }
|
|
||||||
Mod+Ctrl+Right { focus-monitor-right; }
|
|
||||||
Mod+Ctrl+H { focus-monitor-left; }
|
|
||||||
Mod+Ctrl+J { focus-monitor-down; }
|
|
||||||
Mod+Ctrl+K { focus-monitor-up; }
|
|
||||||
Mod+Ctrl+L { focus-monitor-right; }
|
|
||||||
|
|
||||||
// === Move to Monitor ===
|
|
||||||
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
|
||||||
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
|
||||||
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
|
||||||
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
|
||||||
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
|
|
||||||
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
|
||||||
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
|
||||||
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
|
|
||||||
|
|
||||||
// === Workspace Navigation ===
|
|
||||||
Mod+Page_Down { focus-workspace-down; }
|
|
||||||
Mod+Page_Up { focus-workspace-up; }
|
|
||||||
Mod+U { focus-workspace-down; }
|
|
||||||
Mod+I { focus-workspace-up; }
|
|
||||||
Mod+Ctrl+Down { move-column-to-workspace-down; }
|
|
||||||
Mod+Ctrl+Up { move-column-to-workspace-up; }
|
|
||||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
|
||||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
|
||||||
|
|
||||||
// === Move Workspaces ===
|
|
||||||
Mod+Shift+Page_Down { move-workspace-down; }
|
|
||||||
Mod+Shift+Page_Up { move-workspace-up; }
|
|
||||||
Mod+Shift+U { move-workspace-down; }
|
|
||||||
Mod+Shift+I { move-workspace-up; }
|
|
||||||
|
|
||||||
// === Mouse Wheel Navigation ===
|
|
||||||
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
|
||||||
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
|
||||||
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
|
||||||
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
|
||||||
|
|
||||||
Mod+WheelScrollRight { focus-column-right; }
|
|
||||||
Mod+WheelScrollLeft { focus-column-left; }
|
|
||||||
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
|
||||||
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
|
||||||
|
|
||||||
Mod+Shift+WheelScrollDown { focus-column-right; }
|
|
||||||
Mod+Shift+WheelScrollUp { focus-column-left; }
|
|
||||||
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
|
||||||
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
|
||||||
|
|
||||||
// === Numbered Workspaces ===
|
|
||||||
Mod+1 { focus-workspace 1; }
|
|
||||||
Mod+2 { focus-workspace 2; }
|
|
||||||
Mod+3 { focus-workspace 3; }
|
|
||||||
Mod+4 { focus-workspace 4; }
|
|
||||||
Mod+5 { focus-workspace 5; }
|
|
||||||
Mod+6 { focus-workspace 6; }
|
|
||||||
Mod+7 { focus-workspace 7; }
|
|
||||||
Mod+8 { focus-workspace 8; }
|
|
||||||
Mod+9 { focus-workspace 9; }
|
|
||||||
|
|
||||||
// === Move to Numbered Workspaces ===
|
|
||||||
Mod+Shift+1 { move-column-to-workspace 1; }
|
|
||||||
Mod+Shift+2 { move-column-to-workspace 2; }
|
|
||||||
Mod+Shift+3 { move-column-to-workspace 3; }
|
|
||||||
Mod+Shift+4 { move-column-to-workspace 4; }
|
|
||||||
Mod+Shift+5 { move-column-to-workspace 5; }
|
|
||||||
Mod+Shift+6 { move-column-to-workspace 6; }
|
|
||||||
Mod+Shift+7 { move-column-to-workspace 7; }
|
|
||||||
Mod+Shift+8 { move-column-to-workspace 8; }
|
|
||||||
Mod+Shift+9 { move-column-to-workspace 9; }
|
|
||||||
|
|
||||||
// === Column Management ===
|
|
||||||
Mod+BracketLeft { consume-or-expel-window-left; }
|
|
||||||
Mod+BracketRight { consume-or-expel-window-right; }
|
|
||||||
Mod+Period { expel-window-from-column; }
|
|
||||||
|
|
||||||
// === Sizing & Layout ===
|
|
||||||
Mod+R { switch-preset-column-width; }
|
|
||||||
Mod+Shift+R { switch-preset-window-height; }
|
|
||||||
Mod+Ctrl+R { reset-window-height; }
|
|
||||||
Mod+Ctrl+F { expand-column-to-available-width; }
|
|
||||||
Mod+C { center-column; }
|
|
||||||
Mod+Ctrl+C { center-visible-columns; }
|
|
||||||
|
|
||||||
// === Manual Sizing ===
|
|
||||||
Mod+Minus { set-column-width "-10%"; }
|
|
||||||
Mod+Equal { set-column-width "+10%"; }
|
|
||||||
Mod+Shift+Minus { set-window-height "-10%"; }
|
|
||||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
|
||||||
|
|
||||||
// === Screenshots ===
|
|
||||||
XF86Launch1 { screenshot; }
|
|
||||||
Ctrl+XF86Launch1 { screenshot-screen; }
|
|
||||||
Alt+XF86Launch1 { screenshot-window; }
|
|
||||||
Print { screenshot; }
|
|
||||||
Ctrl+Print { screenshot-screen; }
|
|
||||||
Alt+Print { screenshot-window; }
|
|
||||||
// === System Controls ===
|
|
||||||
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
|
||||||
Mod+Shift+P { power-off-monitors; }
|
|
||||||
}
|
|
||||||
debug {
|
debug {
|
||||||
honor-xdg-activation-with-invalid-serial
|
honor-xdg-activation-with-invalid-serial
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override to disable super+tab
|
||||||
|
recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
Alt+Shift+Tab { previous-window scope="output"; }
|
||||||
|
Alt+grave { next-window filter="app-id"; }
|
||||||
|
Alt+Shift+grave { previous-window filter="app-id"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include dms files
|
||||||
|
include "dms/colors.kdl"
|
||||||
|
include "dms/layout.kdl"
|
||||||
|
include "dms/alttab.kdl"
|
||||||
|
include "dms/binds.kdl"
|
||||||
|
|||||||
@@ -4,3 +4,15 @@ import _ "embed"
|
|||||||
|
|
||||||
//go:embed embedded/niri.kdl
|
//go:embed embedded/niri.kdl
|
||||||
var NiriConfig string
|
var NiriConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-colors.kdl
|
||||||
|
var NiriColorsConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-layout.kdl
|
||||||
|
var NiriLayoutConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-alttab.kdl
|
||||||
|
var NiriAlttabConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-binds.kdl
|
||||||
|
var NiriBindsConfig string
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, d.detectMatugen())
|
dependencies = append(dependencies, d.detectMatugen())
|
||||||
dependencies = append(dependencies, d.detectDgop())
|
dependencies = append(dependencies, d.detectDgop())
|
||||||
dependencies = append(dependencies, d.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, d.detectClipboardTools()...)
|
dependencies = append(dependencies, d.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -139,7 +138,12 @@ func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
packages := map[string]PackageMapping{
|
packages := map[string]PackageMapping{
|
||||||
|
// Standard APT packages
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
@@ -148,24 +152,54 @@ func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
|||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
// DMS packages from OBS with variant support
|
||||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
|
// Keep ghostty as manual (no OBS package yet)
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||||
}
|
}
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
niriVariant := variants["niri"]
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
packages["niri"] = d.getNiriMapping(niriVariant)
|
||||||
|
packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhasePrerequisites,
|
Phase: PhasePrerequisites,
|
||||||
@@ -238,8 +272,23 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, obsPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
// Enable OBS repositories
|
||||||
|
if len(obsPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Enabling OBS repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Setting up OBS repositories for additional packages",
|
||||||
|
}
|
||||||
|
if err := d.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable OBS repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -254,6 +303,22 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OBS Packages
|
||||||
|
obsPkgNames := d.extractPackageNames(obsPkgs)
|
||||||
|
if len(obsPkgNames) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := d.installAPTPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Builds
|
||||||
if len(manualPkgs) > 0 {
|
if len(manualPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -297,8 +362,9 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
|
||||||
systemPkgs := []string{}
|
systemPkgs := []string{}
|
||||||
|
obsPkgs := []PackageMapping{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
|
|
||||||
variantMap := make(map[string]deps.PackageVariant)
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
@@ -306,7 +372,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
variantMap[dep.Name] = dep.Variant
|
variantMap[dep.Name] = dep.Variant
|
||||||
}
|
}
|
||||||
|
|
||||||
packageMap := d.GetPackageMapping(wm)
|
packageMap := d.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
for _, dep := range dependencies {
|
for _, dep := range dependencies {
|
||||||
if disabledFlags[dep.Name] {
|
if disabledFlags[dep.Name] {
|
||||||
@@ -326,12 +392,116 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
switch pkgInfo.Repository {
|
switch pkgInfo.Repository {
|
||||||
case RepoTypeSystem:
|
case RepoTypeSystem:
|
||||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeOBS:
|
||||||
|
obsPkgs = append(obsPkgs, pkgInfo)
|
||||||
case RepoTypeManual:
|
case RepoTypeManual:
|
||||||
manualPkgs = append(manualPkgs, dep.Name)
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
osInfo, err := GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get OS info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine Debian version for OBS repository URL
|
||||||
|
debianVersion := "Debian_13"
|
||||||
|
if osInfo.VersionID == "testing" {
|
||||||
|
debianVersion = "Debian_Testing"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range obsPkgs {
|
||||||
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
|
d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||||
|
|
||||||
|
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||||
|
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||||
|
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||||
|
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion)
|
||||||
|
|
||||||
|
// Check if repository already exists
|
||||||
|
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
|
||||||
|
checkCmd := exec.CommandContext(ctx, "test", "-f", listFile)
|
||||||
|
if checkCmd.Run() == nil {
|
||||||
|
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||||
|
|
||||||
|
// Create keyrings directory if it doesn't exist
|
||||||
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
|
if err := mkdirCmd.Run(); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.18,
|
||||||
|
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath)
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
||||||
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add repository
|
||||||
|
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.20,
|
||||||
|
Step: fmt.Sprintf("Adding OBS repository %s...", pkg.RepoURL),
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
|
}
|
||||||
|
|
||||||
|
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile))
|
||||||
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
d.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(enabledRepos) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.25,
|
||||||
|
Step: "Updating package lists...",
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get update",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
|
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
|
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const (
|
|||||||
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
|
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
|
||||||
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
|
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
|
||||||
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
|
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
|
||||||
|
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
||||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
dependencies = append(dependencies, o.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, o.detectClipboardTools()...)
|
dependencies = append(dependencies, o.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -138,13 +137,12 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
|
||||||
|
|
||||||
// Manual builds
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
@@ -156,13 +154,43 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
// Niri stable has native package support on openSUSE
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
niriVariant := variants["niri"]
|
||||||
|
packages["niri"] = o.getNiriMapping(niriVariant)
|
||||||
|
packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if o.commandExists("xwayland-satellite") {
|
if o.commandExists("xwayland-satellite") {
|
||||||
@@ -294,9 +322,23 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, obsPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
// Phase 2: System Packages (Zypper)
|
// Enable OBS repositories
|
||||||
|
if len(obsPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Enabling OBS repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Setting up OBS repositories for additional packages",
|
||||||
|
}
|
||||||
|
if err := o.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable OBS repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: System Packages (Zypper)
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -311,7 +353,22 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Manual Builds
|
// OBS Packages
|
||||||
|
obsPkgNames := o.extractPackageNames(obsPkgs)
|
||||||
|
if len(obsPkgNames) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Builds
|
||||||
if len(manualPkgs) > 0 {
|
if len(manualPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -325,7 +382,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Configuration
|
// Configuration
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseConfiguration,
|
Phase: PhaseConfiguration,
|
||||||
Progress: 0.90,
|
Progress: 0.90,
|
||||||
@@ -334,7 +391,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: Complete
|
// Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
Progress: 1.0,
|
Progress: 1.0,
|
||||||
@@ -346,8 +403,9 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
|
||||||
systemPkgs := []string{}
|
systemPkgs := []string{}
|
||||||
|
obsPkgs := []PackageMapping{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
|
|
||||||
variantMap := make(map[string]deps.PackageVariant)
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
@@ -375,12 +433,80 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
switch pkgInfo.Repository {
|
switch pkgInfo.Repository {
|
||||||
case RepoTypeSystem:
|
case RepoTypeSystem:
|
||||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeOBS:
|
||||||
|
obsPkgs = append(obsPkgs, pkgInfo)
|
||||||
case RepoTypeManual:
|
case RepoTypeManual:
|
||||||
manualPkgs = append(manualPkgs, dep.Name)
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, pkg := range obsPkgs {
|
||||||
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
|
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||||
|
|
||||||
|
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||||
|
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||||
|
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||||
|
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
|
||||||
|
repoPath, pkg.RepoURL)
|
||||||
|
|
||||||
|
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
||||||
|
if checkCmd.Run() == nil {
|
||||||
|
o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.20,
|
||||||
|
Step: fmt.Sprintf("Enabling OBS repo %s...", pkg.RepoURL),
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
o.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh repositories with GPG auto-import
|
||||||
|
if len(enabledRepos) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.25,
|
||||||
|
Step: "Refreshing repositories...",
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||||
|
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
|
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, u.detectMatugen())
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
dependencies = append(dependencies, u.detectDgop())
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
dependencies = append(dependencies, u.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, u.detectClipboardTools()...)
|
dependencies = append(dependencies, u.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -151,6 +150,10 @@ func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return u.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
packages := map[string]PackageMapping{
|
packages := map[string]PackageMapping{
|
||||||
// Standard APT packages
|
// Standard APT packages
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
@@ -160,16 +163,16 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
|||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
|
|
||||||
|
|
||||||
// Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs)
|
// DMS packages from PPAs
|
||||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
// Keep ghostty as manual (no PPA available)
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
@@ -182,13 +185,42 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
|||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
niriVariant := variants["niri"]
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
packages["niri"] = u.getNiriMapping(niriVariant)
|
||||||
|
packages["xwayland-satellite"] = u.getXwaylandSatelliteMapping(niriVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhasePrerequisites,
|
Phase: PhasePrerequisites,
|
||||||
@@ -365,7 +397,7 @@ func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
variantMap[dep.Name] = dep.Variant
|
variantMap[dep.Name] = dep.Variant
|
||||||
}
|
}
|
||||||
|
|
||||||
packageMap := u.GetPackageMapping(wm)
|
packageMap := u.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
for _, dep := range dependencies {
|
for _, dep := range dependencies {
|
||||||
if disabledFlags[dep.Name] {
|
if disabledFlags[dep.Name] {
|
||||||
|
|||||||
@@ -105,14 +105,19 @@ type MenuItem struct {
|
|||||||
|
|
||||||
func NewModel(version string) Model {
|
func NewModel(version string) Model {
|
||||||
detector, _ := NewDetector()
|
detector, _ := NewDetector()
|
||||||
dependencies := detector.GetInstalledComponents()
|
var dependencies []DependencyInfo
|
||||||
|
var hyprlandInstalled, niriInstalled bool
|
||||||
|
var err error
|
||||||
|
if detector != nil {
|
||||||
|
dependencies = detector.GetInstalledComponents()
|
||||||
|
|
||||||
// Use the proper detection method for both window managers
|
// Use the proper detection method for both window managers
|
||||||
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to false if detection fails
|
// Fallback to false if detection fails
|
||||||
hyprlandInstalled = false
|
hyprlandInstalled = false
|
||||||
niriInstalled = false
|
niriInstalled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToggles := make(map[string]bool)
|
updateToggles := make(map[string]bool)
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ func (m Model) renderAboutView() string {
|
|||||||
|
|
||||||
b.WriteString(normalStyle.Render("Components:"))
|
b.WriteString(normalStyle.Render("Components:"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
if len(m.dependencies) == 0 {
|
||||||
|
b.WriteString(normalStyle.Render("\n Component detection not supported on this platform."))
|
||||||
|
}
|
||||||
for _, dep := range m.dependencies {
|
for _, dep := range m.dependencies {
|
||||||
status := "✗"
|
status := "✗"
|
||||||
if dep.Status == 1 {
|
if dep.Status == 1 {
|
||||||
|
|||||||
@@ -87,20 +87,22 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
|||||||
|
|
||||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
|
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
|
||||||
key := h.formatKey(kb)
|
key := h.formatKey(kb)
|
||||||
|
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
|
|
||||||
if desc == "" {
|
if desc == "" {
|
||||||
desc = h.generateDescription(kb.Dispatcher, kb.Params)
|
desc = rawAction
|
||||||
}
|
}
|
||||||
|
|
||||||
return keybinds.Keybind{
|
return keybinds.Keybind{
|
||||||
Key: key,
|
Key: key,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
|
Action: rawAction,
|
||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) generateDescription(dispatcher, params string) string {
|
func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string {
|
||||||
if params != "" {
|
if params != "" {
|
||||||
return dispatcher + " " + params
|
return dispatcher + " " + params
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rawData map[string]interface{}
|
var rawData map[string]any
|
||||||
if err := json.Unmarshal(data, &rawData); err != nil {
|
if err := json.Unmarshal(data, &rawData); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||||||
}
|
}
|
||||||
@@ -63,9 +63,9 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch binds := bindsRaw.(type) {
|
switch binds := bindsRaw.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
for category, categoryBindsRaw := range binds {
|
for category, categoryBindsRaw := range binds {
|
||||||
categoryBindsList, ok := categoryBindsRaw.([]interface{})
|
categoryBindsList, ok := categoryBindsRaw.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -79,11 +79,12 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
categorizedBinds[category] = keybindsList
|
categorizedBinds[category] = keybindsList
|
||||||
}
|
}
|
||||||
|
|
||||||
case []interface{}:
|
case []any:
|
||||||
flatBindsJSON, _ := json.Marshal(binds)
|
flatBindsJSON, _ := json.Marshal(binds)
|
||||||
var flatBinds []struct {
|
var flatBinds []struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Description string `json:"desc"`
|
Description string `json:"desc"`
|
||||||
|
Action string `json:"action,omitempty"`
|
||||||
Category string `json:"cat,omitempty"`
|
Category string `json:"cat,omitempty"`
|
||||||
Subcategory string `json:"subcat,omitempty"`
|
Subcategory string `json:"subcat,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -100,6 +101,7 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
kb := keybinds.Keybind{
|
kb := keybinds.Keybind{
|
||||||
Key: bind.Key,
|
Key: bind.Key,
|
||||||
Description: bind.Description,
|
Description: bind.Description,
|
||||||
|
Action: bind.Action,
|
||||||
Subcategory: bind.Subcategory,
|
Subcategory: bind.Subcategory,
|
||||||
}
|
}
|
||||||
categorizedBinds[category] = append(categorizedBinds[category], kb)
|
categorizedBinds[category] = append(categorizedBinds[category], kb)
|
||||||
|
|||||||
@@ -84,19 +84,21 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
|
|||||||
|
|
||||||
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
|
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
|
||||||
key := m.formatKey(kb)
|
key := m.formatKey(kb)
|
||||||
|
rawAction := m.formatRawAction(kb.Command, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
|
|
||||||
if desc == "" {
|
if desc == "" {
|
||||||
desc = m.generateDescription(kb.Command, kb.Params)
|
desc = rawAction
|
||||||
}
|
}
|
||||||
|
|
||||||
return keybinds.Keybind{
|
return keybinds.Keybind{
|
||||||
Key: key,
|
Key: key,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
|
Action: rawAction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) generateDescription(command, params string) string {
|
func (m *MangoWCProvider) formatRawAction(command, params string) string {
|
||||||
if params != "" {
|
if params != "" {
|
||||||
return command + " " + params
|
return command + " " + params
|
||||||
}
|
}
|
||||||
|
|||||||
564
core/internal/keybinds/providers/niri.go
Normal file
564
core/internal/keybinds/providers/niri.go
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/sblinch/kdl-go"
|
||||||
|
"github.com/sblinch/kdl-go/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NiriProvider struct {
|
||||||
|
configDir string
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
parsed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNiriProvider(configDir string) *NiriProvider {
|
||||||
|
if configDir == "" {
|
||||||
|
configDir = defaultNiriConfigDir()
|
||||||
|
}
|
||||||
|
return &NiriProvider{
|
||||||
|
configDir: configDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultNiriConfigDir() string {
|
||||||
|
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
|
||||||
|
return filepath.Join(configHome, "niri")
|
||||||
|
}
|
||||||
|
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".config", "niri")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) Name() string {
|
||||||
|
return "niri"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||||
|
result, err := ParseNiriKeys(n.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse niri config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
|
n.parsed = true
|
||||||
|
|
||||||
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
|
n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
|
||||||
|
|
||||||
|
sheet := &keybinds.CheatSheet{
|
||||||
|
Title: "Niri Keybinds",
|
||||||
|
Provider: n.Name(),
|
||||||
|
Binds: categorizedBinds,
|
||||||
|
DMSBindsIncluded: result.DMSBindsIncluded,
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.DMSStatus != nil {
|
||||||
|
sheet.DMSStatus = &keybinds.DMSBindsStatus{
|
||||||
|
Exists: result.DMSStatus.Exists,
|
||||||
|
Included: result.DMSStatus.Included,
|
||||||
|
IncludePosition: result.DMSStatus.IncludePosition,
|
||||||
|
TotalIncludes: result.DMSStatus.TotalIncludes,
|
||||||
|
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
|
||||||
|
Effective: result.DMSStatus.Effective,
|
||||||
|
OverriddenBy: result.DMSStatus.OverriddenBy,
|
||||||
|
StatusMessage: result.DMSStatus.StatusMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sheet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) HasDMSBindsIncluded() bool {
|
||||||
|
if n.parsed {
|
||||||
|
return n.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(n.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
n.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
|
n.parsed = true
|
||||||
|
return n.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*NiriKeyBinding) {
|
||||||
|
currentSubcat := subcategory
|
||||||
|
if section.Name != "" {
|
||||||
|
currentSubcat = section.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kb := range section.Keybinds {
|
||||||
|
category := n.categorizeByAction(kb.Action)
|
||||||
|
bind := n.convertKeybind(&kb, currentSubcat, conflicts)
|
||||||
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range section.Children {
|
||||||
|
n.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) categorizeByAction(action string) string {
|
||||||
|
switch {
|
||||||
|
case action == "next-window" || action == "previous-window":
|
||||||
|
return "Alt-Tab"
|
||||||
|
case strings.Contains(action, "screenshot"):
|
||||||
|
return "Screenshot"
|
||||||
|
case action == "show-hotkey-overlay" || action == "toggle-overview":
|
||||||
|
return "Overview"
|
||||||
|
case action == "quit" ||
|
||||||
|
action == "power-off-monitors" ||
|
||||||
|
action == "toggle-keyboard-shortcuts-inhibit" ||
|
||||||
|
strings.Contains(action, "dpms"):
|
||||||
|
return "System"
|
||||||
|
case action == "spawn":
|
||||||
|
return "Execute"
|
||||||
|
case strings.Contains(action, "workspace"):
|
||||||
|
return "Workspace"
|
||||||
|
case strings.HasPrefix(action, "focus-monitor") ||
|
||||||
|
strings.HasPrefix(action, "move-column-to-monitor") ||
|
||||||
|
strings.HasPrefix(action, "move-window-to-monitor"):
|
||||||
|
return "Monitor"
|
||||||
|
case strings.Contains(action, "window") ||
|
||||||
|
strings.Contains(action, "focus") ||
|
||||||
|
strings.Contains(action, "move") ||
|
||||||
|
strings.Contains(action, "swap") ||
|
||||||
|
strings.Contains(action, "resize") ||
|
||||||
|
strings.Contains(action, "column"):
|
||||||
|
return "Window"
|
||||||
|
default:
|
||||||
|
return "Other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind {
|
||||||
|
rawAction := n.formatRawAction(kb.Action, kb.Args)
|
||||||
|
keyStr := n.formatKey(kb)
|
||||||
|
|
||||||
|
source := "config"
|
||||||
|
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
||||||
|
source = "dms"
|
||||||
|
}
|
||||||
|
|
||||||
|
bind := keybinds.Keybind{
|
||||||
|
Key: keyStr,
|
||||||
|
Description: kb.Description,
|
||||||
|
Action: rawAction,
|
||||||
|
Subcategory: subcategory,
|
||||||
|
Source: source,
|
||||||
|
}
|
||||||
|
|
||||||
|
if source == "dms" && conflicts != nil {
|
||||||
|
if conflictKb, ok := conflicts[keyStr]; ok {
|
||||||
|
bind.Conflict = &keybinds.Keybind{
|
||||||
|
Key: keyStr,
|
||||||
|
Description: conflictKb.Description,
|
||||||
|
Action: n.formatRawAction(conflictKb.Action, conflictKb.Args),
|
||||||
|
Source: "config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) formatRawAction(action string, args []string) string {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "spawn" && len(args) >= 3 && args[1] == "-c" {
|
||||||
|
switch args[0] {
|
||||||
|
case "sh", "bash":
|
||||||
|
cmd := strings.Join(args[2:], " ")
|
||||||
|
return fmt.Sprintf("spawn %s -c \"%s\"", args[0], strings.ReplaceAll(cmd, "\"", "\\\""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return action + " " + strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
|
||||||
|
parts := make([]string, 0, len(kb.Mods)+1)
|
||||||
|
parts = append(parts, kb.Mods...)
|
||||||
|
parts = append(parts, kb.Key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) GetOverridePath() string {
|
||||||
|
return filepath.Join(n.configDir, "dms", "binds.kdl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) validateAction(action string) error {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
switch {
|
||||||
|
case action == "":
|
||||||
|
return fmt.Errorf("action cannot be empty")
|
||||||
|
case action == "spawn" || action == "spawn ":
|
||||||
|
return fmt.Errorf("spawn command requires arguments")
|
||||||
|
case strings.HasPrefix(action, "spawn "):
|
||||||
|
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn "))
|
||||||
|
switch rest {
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("spawn command requires arguments")
|
||||||
|
case "sh -c \"\"", "sh -c ''", "bash -c \"\"", "bash -c ''":
|
||||||
|
return fmt.Errorf("shell command cannot be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) SetBind(key, action, description string, options map[string]any) error {
|
||||||
|
if err := n.validateAction(action); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
overridePath := n.GetOverridePath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingBinds, err := n.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
existingBinds = make(map[string]*overrideBind)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingBinds[key] = &overrideBind{
|
||||||
|
Key: key,
|
||||||
|
Action: action,
|
||||||
|
Description: description,
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) RemoveBind(key string) error {
|
||||||
|
existingBinds, err := n.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(existingBinds, key)
|
||||||
|
return n.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
type overrideBind struct {
|
||||||
|
Key string
|
||||||
|
Action string
|
||||||
|
Description string
|
||||||
|
Options map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
||||||
|
overridePath := n.GetOverridePath()
|
||||||
|
binds := make(map[string]*overrideBind)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(overridePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||||
|
parser.currentSource = overridePath
|
||||||
|
|
||||||
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range doc.Nodes {
|
||||||
|
if node.Name.String() != "binds" || node.Children == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, child := range node.Children {
|
||||||
|
kb := parser.parseKeybindNode(child, "")
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyStr := parser.formatBindKey(kb)
|
||||||
|
binds[keyStr] = &overrideBind{
|
||||||
|
Key: keyStr,
|
||||||
|
Action: n.formatRawAction(kb.Action, kb.Args),
|
||||||
|
Description: kb.Description,
|
||||||
|
Options: n.extractOptions(child),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
|
||||||
|
if node.Properties == nil {
|
||||||
|
return make(map[string]any)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := make(map[string]any)
|
||||||
|
if val, ok := node.Properties.Get("repeat"); ok {
|
||||||
|
opts["repeat"] = val.String() == "true"
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("cooldown-ms"); ok {
|
||||||
|
opts["cooldown-ms"] = val.String()
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("allow-when-locked"); ok {
|
||||||
|
opts["allow-when-locked"] = val.String() == "true"
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) isRecentWindowsAction(action string) bool {
|
||||||
|
switch action {
|
||||||
|
case "next-window", "previous-window":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
|
||||||
|
node := document.NewNode()
|
||||||
|
node.SetName(bind.Key)
|
||||||
|
|
||||||
|
if bind.Options != nil {
|
||||||
|
if v, ok := bind.Options["repeat"]; ok && v == false {
|
||||||
|
node.AddProperty("repeat", false, "")
|
||||||
|
}
|
||||||
|
if v, ok := bind.Options["cooldown-ms"]; ok {
|
||||||
|
node.AddProperty("cooldown-ms", v, "")
|
||||||
|
}
|
||||||
|
if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
|
||||||
|
node.AddProperty("allow-when-locked", true, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bind.Description != "" {
|
||||||
|
node.AddProperty("hotkey-overlay-title", bind.Description, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
actionNode := n.buildActionNode(bind.Action)
|
||||||
|
node.AddNode(actionNode)
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
node := document.NewNode()
|
||||||
|
|
||||||
|
parts := n.parseActionParts(action)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
node.SetName(action)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
node.SetName(parts[0])
|
||||||
|
for _, arg := range parts[1:] {
|
||||||
|
node.AddArgument(arg, "")
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) parseActionParts(action string) []string {
|
||||||
|
var parts []string
|
||||||
|
var current strings.Builder
|
||||||
|
var inQuote, escaped bool
|
||||||
|
|
||||||
|
for _, r := range action {
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
|
current.WriteRune(r)
|
||||||
|
escaped = false
|
||||||
|
case r == '\\':
|
||||||
|
escaped = true
|
||||||
|
case r == '"':
|
||||||
|
inQuote = !inQuote
|
||||||
|
case r == ' ' && !inQuote:
|
||||||
|
if current.Len() > 0 {
|
||||||
|
parts = append(parts, current.String())
|
||||||
|
current.Reset()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
current.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current.Len() > 0 {
|
||||||
|
parts = append(parts, current.String())
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
|
||||||
|
overridePath := n.GetOverridePath()
|
||||||
|
content := n.generateBindsContent(binds)
|
||||||
|
|
||||||
|
if err := n.validateBindsContent(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(overridePath, []byte(content), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) getBindSortPriority(action string) int {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
|
||||||
|
return 0
|
||||||
|
case strings.Contains(action, "workspace"):
|
||||||
|
return 1
|
||||||
|
case strings.Contains(action, "window") || strings.Contains(action, "column") ||
|
||||||
|
strings.Contains(action, "focus") || strings.Contains(action, "move") ||
|
||||||
|
strings.Contains(action, "swap") || strings.Contains(action, "resize"):
|
||||||
|
return 2
|
||||||
|
case strings.HasPrefix(action, "focus-monitor") || strings.Contains(action, "monitor"):
|
||||||
|
return 3
|
||||||
|
case strings.Contains(action, "screenshot"):
|
||||||
|
return 4
|
||||||
|
case action == "quit" || action == "power-off-monitors" || strings.Contains(action, "dpms"):
|
||||||
|
return 5
|
||||||
|
case strings.HasPrefix(action, "spawn"):
|
||||||
|
return 6
|
||||||
|
default:
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
|
||||||
|
if len(binds) == 0 {
|
||||||
|
return "binds {}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
var regularBinds, recentWindowsBinds []*overrideBind
|
||||||
|
for _, bind := range binds {
|
||||||
|
switch {
|
||||||
|
case n.isRecentWindowsAction(bind.Action):
|
||||||
|
recentWindowsBinds = append(recentWindowsBinds, bind)
|
||||||
|
default:
|
||||||
|
regularBinds = append(regularBinds, bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(regularBinds, func(i, j int) bool {
|
||||||
|
pi, pj := n.getBindSortPriority(regularBinds[i].Action), n.getBindSortPriority(regularBinds[j].Action)
|
||||||
|
if pi != pj {
|
||||||
|
return pi < pj
|
||||||
|
}
|
||||||
|
return regularBinds[i].Key < regularBinds[j].Key
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Slice(recentWindowsBinds, func(i, j int) bool {
|
||||||
|
return recentWindowsBinds[i].Key < recentWindowsBinds[j].Key
|
||||||
|
})
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString("binds {\n")
|
||||||
|
for _, bind := range regularBinds {
|
||||||
|
n.writeBindNode(&sb, bind, " ")
|
||||||
|
}
|
||||||
|
sb.WriteString("}\n")
|
||||||
|
|
||||||
|
if len(recentWindowsBinds) > 0 {
|
||||||
|
sb.WriteString("\nrecent-windows {\n")
|
||||||
|
sb.WriteString(" binds {\n")
|
||||||
|
for _, bind := range recentWindowsBinds {
|
||||||
|
n.writeBindNode(&sb, bind, " ")
|
||||||
|
}
|
||||||
|
sb.WriteString(" }\n")
|
||||||
|
sb.WriteString("}\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, indent string) {
|
||||||
|
node := n.buildBindNode(bind)
|
||||||
|
|
||||||
|
sb.WriteString(indent)
|
||||||
|
sb.WriteString(node.Name.String())
|
||||||
|
|
||||||
|
if node.Properties.Exist() {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(strings.TrimLeft(node.Properties.String(), " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString(" { ")
|
||||||
|
if len(node.Children) > 0 {
|
||||||
|
child := node.Children[0]
|
||||||
|
actionName := child.Name.String()
|
||||||
|
sb.WriteString(actionName)
|
||||||
|
forceQuote := actionName == "spawn"
|
||||||
|
for _, arg := range child.Arguments {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
n.writeArg(sb, arg.ValueString(), forceQuote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString("; }\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) {
|
||||||
|
if !forceQuote && n.isNumericArg(val) {
|
||||||
|
sb.WriteString(val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sb.WriteString("\"")
|
||||||
|
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
|
||||||
|
sb.WriteString("\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) isNumericArg(val string) bool {
|
||||||
|
if val == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
start := 0
|
||||||
|
if val[0] == '-' || val[0] == '+' {
|
||||||
|
if len(val) == 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
start = 1
|
||||||
|
}
|
||||||
|
for i := start; i < len(val); i++ {
|
||||||
|
if val[i] < '0' || val[i] > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) validateBindsContent(content string) error {
|
||||||
|
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpFile.WriteString(content); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("failed to write temp file: %w", err)
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command("niri", "validate", "-c", tmpFile.Name())
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid config: %s", strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
358
core/internal/keybinds/providers/niri_parser.go
Normal file
358
core/internal/keybinds/providers/niri_parser.go
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sblinch/kdl-go"
|
||||||
|
"github.com/sblinch/kdl-go/document"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NiriKeyBinding struct {
|
||||||
|
Mods []string
|
||||||
|
Key string
|
||||||
|
Action string
|
||||||
|
Args []string
|
||||||
|
Description string
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NiriSection struct {
|
||||||
|
Name string
|
||||||
|
Keybinds []NiriKeyBinding
|
||||||
|
Children []NiriSection
|
||||||
|
}
|
||||||
|
|
||||||
|
type NiriParser struct {
|
||||||
|
configDir string
|
||||||
|
processedFiles map[string]bool
|
||||||
|
bindMap map[string]*NiriKeyBinding
|
||||||
|
bindOrder []string
|
||||||
|
currentSource string
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
dmsBindsExists bool
|
||||||
|
includeCount int
|
||||||
|
dmsIncludePos int
|
||||||
|
bindsBeforeDMS int
|
||||||
|
bindsAfterDMS int
|
||||||
|
dmsBindKeys map[string]bool
|
||||||
|
configBindKeys map[string]bool
|
||||||
|
dmsProcessed bool
|
||||||
|
dmsBindMap map[string]*NiriKeyBinding
|
||||||
|
conflictingConfigs map[string]*NiriKeyBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
|
return &NiriParser{
|
||||||
|
configDir: configDir,
|
||||||
|
processedFiles: make(map[string]bool),
|
||||||
|
bindMap: make(map[string]*NiriKeyBinding),
|
||||||
|
bindOrder: []string{},
|
||||||
|
currentSource: "",
|
||||||
|
dmsIncludePos: -1,
|
||||||
|
dmsBindKeys: make(map[string]bool),
|
||||||
|
configBindKeys: make(map[string]bool),
|
||||||
|
dmsBindMap: make(map[string]*NiriKeyBinding),
|
||||||
|
conflictingConfigs: make(map[string]*NiriKeyBinding),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) Parse() (*NiriSection, error) {
|
||||||
|
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
|
||||||
|
if _, err := os.Stat(dmsBindsPath); err == nil {
|
||||||
|
p.dmsBindsExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(p.configDir, "config.kdl")
|
||||||
|
section, err := p.parseFile(configPath, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.dmsBindsExists && !p.dmsProcessed {
|
||||||
|
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
section.Keybinds = p.finalizeBinds()
|
||||||
|
return section, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSection) {
|
||||||
|
data, err := os.ReadFile(dmsBindsPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = dmsBindsPath
|
||||||
|
baseDir := filepath.Dir(dmsBindsPath)
|
||||||
|
p.processNodes(doc.Nodes, section, baseDir)
|
||||||
|
p.currentSource = prevSource
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
|
||||||
|
binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
|
||||||
|
for _, key := range p.bindOrder {
|
||||||
|
if kb, ok := p.bindMap[key]; ok {
|
||||||
|
binds = append(binds, *kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return binds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
|
||||||
|
key := p.formatBindKey(kb)
|
||||||
|
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
|
||||||
|
|
||||||
|
if isDMSBind {
|
||||||
|
p.dmsBindKeys[key] = true
|
||||||
|
p.dmsBindMap[key] = kb
|
||||||
|
} else if p.dmsBindKeys[key] {
|
||||||
|
p.bindsAfterDMS++
|
||||||
|
p.conflictingConfigs[key] = kb
|
||||||
|
p.configBindKeys[key] = true
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
p.configBindKeys[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := p.bindMap[key]; !exists {
|
||||||
|
p.bindOrder = append(p.bindOrder, key)
|
||||||
|
}
|
||||||
|
p.bindMap[key] = kb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string {
|
||||||
|
parts := make([]string, 0, len(kb.Mods)+1)
|
||||||
|
parts = append(parts, kb.Mods...)
|
||||||
|
parts = append(parts, kb.Key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, error) {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve path %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.processedFiles[absPath] {
|
||||||
|
return &NiriSection{Name: sectionName}, nil
|
||||||
|
}
|
||||||
|
p.processedFiles[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
section := &NiriSection{
|
||||||
|
Name: sectionName,
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
p.processNodes(doc.Nodes, section, baseDir)
|
||||||
|
p.currentSource = prevSource
|
||||||
|
|
||||||
|
return section, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection, baseDir string) {
|
||||||
|
for _, node := range nodes {
|
||||||
|
name := node.Name.String()
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "include":
|
||||||
|
p.handleInclude(node, section, baseDir)
|
||||||
|
case "binds":
|
||||||
|
p.extractBinds(node, section, "")
|
||||||
|
case "recent-windows":
|
||||||
|
p.handleRecentWindows(node, section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) {
|
||||||
|
if len(node.Arguments) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath := strings.Trim(node.Arguments[0].String(), "\"")
|
||||||
|
isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl")
|
||||||
|
|
||||||
|
p.includeCount++
|
||||||
|
if isDMSInclude {
|
||||||
|
p.dmsBindsIncluded = true
|
||||||
|
p.dmsIncludePos = p.includeCount
|
||||||
|
p.bindsBeforeDMS = len(p.bindMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(baseDir, includePath)
|
||||||
|
if filepath.IsAbs(includePath) {
|
||||||
|
fullPath = includePath
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDMSInclude {
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
includedSection, err := p.parseFile(fullPath, "")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
section.Children = append(section.Children, includedSection.Children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) HasDMSBindsIncluded() bool {
|
||||||
|
return p.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
|
||||||
|
if node.Children == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
if child.Name.String() != "binds" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.extractBinds(child, section, "Alt-Tab")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) extractBinds(node *document.Node, section *NiriSection, subcategory string) {
|
||||||
|
if node.Children == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
kb := p.parseKeybindNode(child, subcategory)
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.addBind(kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBinding {
|
||||||
|
keyCombo := node.Name.String()
|
||||||
|
if keyCombo == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mods, key := p.parseKeyCombo(keyCombo)
|
||||||
|
|
||||||
|
var action string
|
||||||
|
var args []string
|
||||||
|
if len(node.Children) > 0 {
|
||||||
|
actionNode := node.Children[0]
|
||||||
|
action = actionNode.Name.String()
|
||||||
|
for _, arg := range actionNode.Arguments {
|
||||||
|
args = append(args, arg.ValueString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description string
|
||||||
|
if node.Properties != nil {
|
||||||
|
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
||||||
|
description = val.ValueString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NiriKeyBinding{
|
||||||
|
Mods: mods,
|
||||||
|
Key: key,
|
||||||
|
Action: action,
|
||||||
|
Args: args,
|
||||||
|
Description: description,
|
||||||
|
Source: p.currentSource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
|
||||||
|
parts := strings.Split(combo, "+")
|
||||||
|
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
|
return nil, combo
|
||||||
|
case 1:
|
||||||
|
return nil, parts[0]
|
||||||
|
default:
|
||||||
|
return parts[:len(parts)-1], parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NiriParseResult struct {
|
||||||
|
Section *NiriSection
|
||||||
|
DMSBindsIncluded bool
|
||||||
|
DMSStatus *DMSBindsStatusInfo
|
||||||
|
ConflictingConfigs map[string]*NiriKeyBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
type DMSBindsStatusInfo struct {
|
||||||
|
Exists bool
|
||||||
|
Included bool
|
||||||
|
IncludePosition int
|
||||||
|
TotalIncludes int
|
||||||
|
BindsAfterDMS int
|
||||||
|
Effective bool
|
||||||
|
OverriddenBy int
|
||||||
|
StatusMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) buildDMSStatus() *DMSBindsStatusInfo {
|
||||||
|
status := &DMSBindsStatusInfo{
|
||||||
|
Exists: p.dmsBindsExists,
|
||||||
|
Included: p.dmsBindsIncluded,
|
||||||
|
IncludePosition: p.dmsIncludePos,
|
||||||
|
TotalIncludes: p.includeCount,
|
||||||
|
BindsAfterDMS: p.bindsAfterDMS,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !p.dmsBindsExists:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/binds.kdl does not exist"
|
||||||
|
case !p.dmsBindsIncluded:
|
||||||
|
status.Effective = false
|
||||||
|
status.StatusMessage = "dms/binds.kdl is not included in config.kdl"
|
||||||
|
case p.bindsAfterDMS > 0:
|
||||||
|
status.Effective = true
|
||||||
|
status.OverriddenBy = p.bindsAfterDMS
|
||||||
|
status.StatusMessage = "Some DMS binds may be overridden by config binds"
|
||||||
|
default:
|
||||||
|
status.Effective = true
|
||||||
|
status.StatusMessage = "DMS binds are active"
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
|
||||||
|
parser := NewNiriParser(configDir)
|
||||||
|
section, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &NiriParseResult{
|
||||||
|
Section: section,
|
||||||
|
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
|
||||||
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
614
core/internal/keybinds/providers/niri_parser_test.go
Normal file
614
core/internal/keybinds/providers/niri_parser_test.go
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNiriParseKeyCombo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
combo string
|
||||||
|
expectedMods []string
|
||||||
|
expectedKey string
|
||||||
|
}{
|
||||||
|
{"Mod+Q", []string{"Mod"}, "Q"},
|
||||||
|
{"Mod+Shift+F", []string{"Mod", "Shift"}, "F"},
|
||||||
|
{"Ctrl+Alt+Delete", []string{"Ctrl", "Alt"}, "Delete"},
|
||||||
|
{"Print", nil, "Print"},
|
||||||
|
{"XF86AudioMute", nil, "XF86AudioMute"},
|
||||||
|
{"Super+Tab", []string{"Super"}, "Tab"},
|
||||||
|
{"Mod+Shift+Ctrl+H", []string{"Mod", "Shift", "Ctrl"}, "H"},
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := NewNiriParser("")
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.combo, func(t *testing.T) {
|
||||||
|
mods, key := parser.parseKeyCombo(tt.combo)
|
||||||
|
|
||||||
|
if len(mods) != len(tt.expectedMods) {
|
||||||
|
t.Errorf("Mods length = %d, want %d", len(mods), len(tt.expectedMods))
|
||||||
|
} else {
|
||||||
|
for i := range mods {
|
||||||
|
if mods[i] != tt.expectedMods[i] {
|
||||||
|
t.Errorf("Mods[%d] = %q, want %q", i, mods[i], tt.expectedMods[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != tt.expectedKey {
|
||||||
|
t.Errorf("Key = %q, want %q", key, tt.expectedKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseBasicBinds(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+Q { close-window; }
|
||||||
|
Mod+F { fullscreen-window; }
|
||||||
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundClose := false
|
||||||
|
foundFullscreen := false
|
||||||
|
foundTerminal := false
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch kb.Action {
|
||||||
|
case "close-window":
|
||||||
|
foundClose = true
|
||||||
|
if kb.Key != "Q" || len(kb.Mods) != 1 || kb.Mods[0] != "Mod" {
|
||||||
|
t.Errorf("close-window keybind mismatch: %+v", kb)
|
||||||
|
}
|
||||||
|
case "fullscreen-window":
|
||||||
|
foundFullscreen = true
|
||||||
|
case "spawn":
|
||||||
|
foundTerminal = true
|
||||||
|
if kb.Description != "Open Terminal" {
|
||||||
|
t.Errorf("spawn description = %q, want %q", kb.Description, "Open Terminal")
|
||||||
|
}
|
||||||
|
if len(kb.Args) != 1 || kb.Args[0] != "kitty" {
|
||||||
|
t.Errorf("spawn args = %v, want [kitty]", kb.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundClose {
|
||||||
|
t.Error("close-window keybind not found")
|
||||||
|
}
|
||||||
|
if !foundFullscreen {
|
||||||
|
t.Error("fullscreen-window keybind not found")
|
||||||
|
}
|
||||||
|
if !foundTerminal {
|
||||||
|
t.Error("spawn keybind not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseRecentWindows(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
Alt+Shift+Tab { previous-window scope="output"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 2 {
|
||||||
|
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundNext := false
|
||||||
|
foundPrev := false
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch kb.Action {
|
||||||
|
case "next-window":
|
||||||
|
foundNext = true
|
||||||
|
case "previous-window":
|
||||||
|
foundPrev = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundNext {
|
||||||
|
t.Error("next-window keybind not found")
|
||||||
|
}
|
||||||
|
if !foundPrev {
|
||||||
|
t.Error("previous-window keybind not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseInclude(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
includeConfig := filepath.Join(subDir, "binds.kdl")
|
||||||
|
|
||||||
|
mainContent := `binds {
|
||||||
|
Mod+Q { close-window; }
|
||||||
|
}
|
||||||
|
include "dms/binds.kdl"
|
||||||
|
`
|
||||||
|
includeContent := `binds {
|
||||||
|
Mod+T hotkey-overlay-title="Terminal" { spawn "kitty"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 2 {
|
||||||
|
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseIncludeOverride(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
includeConfig := filepath.Join(subDir, "binds.kdl")
|
||||||
|
|
||||||
|
mainContent := `binds {
|
||||||
|
Mod+T hotkey-overlay-title="Main Terminal" { spawn "alacritty"; }
|
||||||
|
}
|
||||||
|
include "dms/binds.kdl"
|
||||||
|
`
|
||||||
|
includeContent := `binds {
|
||||||
|
Mod+T hotkey-overlay-title="Override Terminal" { spawn "kitty"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 1 {
|
||||||
|
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) > 0 {
|
||||||
|
kb := result.Section.Keybinds[0]
|
||||||
|
if kb.Description != "Override Terminal" {
|
||||||
|
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
|
||||||
|
}
|
||||||
|
if len(kb.Args) != 1 || kb.Args[0] != "kitty" {
|
||||||
|
t.Errorf("Expected args [kitty] (from include), got %v", kb.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseCircularInclude(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
otherConfig := filepath.Join(tmpDir, "other.kdl")
|
||||||
|
|
||||||
|
mainContent := `binds {
|
||||||
|
Mod+Q { close-window; }
|
||||||
|
}
|
||||||
|
include "other.kdl"
|
||||||
|
`
|
||||||
|
otherContent := `binds {
|
||||||
|
Mod+T { spawn "kitty"; }
|
||||||
|
}
|
||||||
|
include "config.kdl"
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(otherConfig, []byte(otherContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write other config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 2 {
|
||||||
|
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseMissingInclude(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+Q { close-window; }
|
||||||
|
}
|
||||||
|
include "nonexistent/file.kdl"
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 1 {
|
||||||
|
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseNoBinds(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `cursor {
|
||||||
|
xcursor-theme "Bibata"
|
||||||
|
xcursor-size 24
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
numlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 0 {
|
||||||
|
t.Errorf("Expected 0 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseErrors(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nonexistent_directory",
|
||||||
|
path: "/nonexistent/path/that/does/not/exist",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := ParseNiriKeys(tt.path)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriBindOverrideBehavior(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+T hotkey-overlay-title="First" { spawn "first"; }
|
||||||
|
Mod+Q { close-window; }
|
||||||
|
Mod+T hotkey-overlay-title="Second" { spawn "second"; }
|
||||||
|
Mod+F { fullscreen-window; }
|
||||||
|
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Fatalf("Expected 3 unique keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
var modT *NiriKeyBinding
|
||||||
|
for i := range result.Section.Keybinds {
|
||||||
|
kb := &result.Section.Keybinds[i]
|
||||||
|
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
|
||||||
|
modT = kb
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if modT == nil {
|
||||||
|
t.Fatal("Mod+T keybind not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if modT.Description != "Third" {
|
||||||
|
t.Errorf("Mod+T description = %q, want 'Third' (last definition wins)", modT.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(modT.Args) != 1 || modT.Args[0] != "third" {
|
||||||
|
t.Errorf("Mod+T args = %v, want [third] (last definition wins)", modT.Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
subDir := filepath.Join(tmpDir, "custom")
|
||||||
|
if err := os.MkdirAll(subDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
includeConfig := filepath.Join(subDir, "overrides.kdl")
|
||||||
|
|
||||||
|
mainContent := `binds {
|
||||||
|
Mod+1 { focus-workspace 1; }
|
||||||
|
Mod+2 { focus-workspace 2; }
|
||||||
|
Mod+T hotkey-overlay-title="Default Terminal" { spawn "xterm"; }
|
||||||
|
}
|
||||||
|
include "custom/overrides.kdl"
|
||||||
|
binds {
|
||||||
|
Mod+3 { focus-workspace 3; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
includeContent := `binds {
|
||||||
|
Mod+T hotkey-overlay-title="Custom Terminal" { spawn "kitty"; }
|
||||||
|
Mod+2 { focus-workspace 22; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 4 {
|
||||||
|
t.Errorf("Expected 4 unique keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
bindMap := make(map[string]*NiriKeyBinding)
|
||||||
|
for i := range result.Section.Keybinds {
|
||||||
|
kb := &result.Section.Keybinds[i]
|
||||||
|
key := ""
|
||||||
|
for _, m := range kb.Mods {
|
||||||
|
key += m + "+"
|
||||||
|
}
|
||||||
|
key += kb.Key
|
||||||
|
bindMap[key] = kb
|
||||||
|
}
|
||||||
|
|
||||||
|
if kb, ok := bindMap["Mod+T"]; ok {
|
||||||
|
if kb.Description != "Custom Terminal" {
|
||||||
|
t.Errorf("Mod+T should be overridden by include, got description %q", kb.Description)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Error("Mod+T not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if kb, ok := bindMap["Mod+2"]; ok {
|
||||||
|
if len(kb.Args) != 1 || kb.Args[0] != "22" {
|
||||||
|
t.Errorf("Mod+2 should be overridden by include with workspace 22, got args %v", kb.Args)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Error("Mod+2 not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := bindMap["Mod+1"]; !ok {
|
||||||
|
t.Error("Mod+1 should exist (not overridden)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := bindMap["Mod+3"]; !ok {
|
||||||
|
t.Error("Mod+3 should exist (added after include)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseMultipleArgs(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||||
|
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 1 {
|
||||||
|
t.Fatalf("Expected 1 keybind, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
kb := result.Section.Keybinds[0]
|
||||||
|
if len(kb.Args) != 5 {
|
||||||
|
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedArgs := []string{"dms", "ipc", "call", "spotlight", "toggle"}
|
||||||
|
for i, arg := range expectedArgs {
|
||||||
|
if i < len(kb.Args) && kb.Args[i] != arg {
|
||||||
|
t.Errorf("Args[%d] = %q, want %q", i, kb.Args[i], arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
||||||
|
Mod+2 hotkey-overlay-title="Focus Workspace 2" { focus-workspace 2; }
|
||||||
|
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
||||||
|
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 4 {
|
||||||
|
t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch kb.Key {
|
||||||
|
case "1":
|
||||||
|
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" {
|
||||||
|
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "1" {
|
||||||
|
t.Errorf("Mod+1 action/args mismatch: %+v", kb)
|
||||||
|
}
|
||||||
|
if kb.Description != "Focus Workspace 1" {
|
||||||
|
t.Errorf("Mod+1 description = %q, want 'Focus Workspace 1'", kb.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "0":
|
||||||
|
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "10" {
|
||||||
|
t.Errorf("Mod+0 action/args mismatch: %+v", kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseQuotedStringArgs(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
||||||
|
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
||||||
|
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
if kb.Action == "set-column-width" {
|
||||||
|
if len(kb.Args) != 1 {
|
||||||
|
t.Errorf("set-column-width should have 1 arg, got %d", len(kb.Args))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kb.Args[0] != "-10%" && kb.Args[0] != "+10%" {
|
||||||
|
t.Errorf("set-column-width arg = %q, want -10%% or +10%%", kb.Args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseActionWithProperties(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; }
|
||||||
|
Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; }
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); 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 len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch kb.Action {
|
||||||
|
case "move-column-to-workspace":
|
||||||
|
if len(kb.Args) != 1 {
|
||||||
|
t.Errorf("move-column-to-workspace should have 1 arg, got %d", len(kb.Args))
|
||||||
|
}
|
||||||
|
case "next-window":
|
||||||
|
if kb.Key != "Tab" {
|
||||||
|
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
607
core/internal/keybinds/providers/niri_test.go
Normal file
607
core/internal/keybinds/providers/niri_test.go
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNiriProviderName(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
if provider.Name() != "niri" {
|
||||||
|
t.Errorf("Name() = %q, want %q", provider.Name(), "niri")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriProviderGetCheatSheet(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+Q { close-window; }
|
||||||
|
Mod+F { fullscreen-window; }
|
||||||
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
|
Mod+1 { focus-workspace 1; }
|
||||||
|
Mod+Shift+1 { move-column-to-workspace 1; }
|
||||||
|
Print { screenshot; }
|
||||||
|
Mod+Shift+E { quit; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewNiriProvider(tmpDir)
|
||||||
|
cheatSheet, err := provider.GetCheatSheet()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cheatSheet.Title != "Niri Keybinds" {
|
||||||
|
t.Errorf("Title = %q, want %q", cheatSheet.Title, "Niri Keybinds")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cheatSheet.Provider != "niri" {
|
||||||
|
t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri")
|
||||||
|
}
|
||||||
|
|
||||||
|
windowBinds := cheatSheet.Binds["Window"]
|
||||||
|
if len(windowBinds) < 2 {
|
||||||
|
t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
execBinds := cheatSheet.Binds["Execute"]
|
||||||
|
if len(execBinds) < 1 {
|
||||||
|
t.Errorf("Expected at least 1 Execute bind, got %d", len(execBinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceBinds := cheatSheet.Binds["Workspace"]
|
||||||
|
if len(workspaceBinds) < 2 {
|
||||||
|
t.Errorf("Expected at least 2 Workspace binds, got %d", len(workspaceBinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshotBinds := cheatSheet.Binds["Screenshot"]
|
||||||
|
if len(screenshotBinds) < 1 {
|
||||||
|
t.Errorf("Expected at least 1 Screenshot bind, got %d", len(screenshotBinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
systemBinds := cheatSheet.Binds["System"]
|
||||||
|
if len(systemBinds) < 1 {
|
||||||
|
t.Errorf("Expected at least 1 System bind, got %d", len(systemBinds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriCategorizeByAction(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
action string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"focus-workspace", "Workspace"},
|
||||||
|
{"focus-workspace-up", "Workspace"},
|
||||||
|
{"move-column-to-workspace", "Workspace"},
|
||||||
|
{"focus-monitor-left", "Monitor"},
|
||||||
|
{"move-column-to-monitor-right", "Monitor"},
|
||||||
|
{"close-window", "Window"},
|
||||||
|
{"fullscreen-window", "Window"},
|
||||||
|
{"maximize-column", "Window"},
|
||||||
|
{"toggle-window-floating", "Window"},
|
||||||
|
{"focus-column-left", "Window"},
|
||||||
|
{"move-column-right", "Window"},
|
||||||
|
{"spawn", "Execute"},
|
||||||
|
{"quit", "System"},
|
||||||
|
{"power-off-monitors", "System"},
|
||||||
|
{"screenshot", "Screenshot"},
|
||||||
|
{"screenshot-window", "Screenshot"},
|
||||||
|
{"toggle-overview", "Overview"},
|
||||||
|
{"show-hotkey-overlay", "Overview"},
|
||||||
|
{"next-window", "Alt-Tab"},
|
||||||
|
{"previous-window", "Alt-Tab"},
|
||||||
|
{"unknown-action", "Other"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.action, func(t *testing.T) {
|
||||||
|
result := provider.categorizeByAction(tt.action)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("categorizeByAction(%q) = %q, want %q", tt.action, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriFormatRawAction(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
action string
|
||||||
|
args []string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"spawn", []string{"kitty"}, "spawn kitty"},
|
||||||
|
{"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"},
|
||||||
|
{"close-window", nil, "close-window"},
|
||||||
|
{"fullscreen-window", nil, "fullscreen-window"},
|
||||||
|
{"focus-workspace", []string{"1"}, "focus-workspace 1"},
|
||||||
|
{"move-column-to-workspace", []string{"5"}, "move-column-to-workspace 5"},
|
||||||
|
{"set-column-width", []string{"+10%"}, "set-column-width +10%"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.action, func(t *testing.T) {
|
||||||
|
result := provider.formatRawAction(tt.action, tt.args)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("formatRawAction(%q, %v) = %q, want %q", tt.action, tt.args, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriFormatKey(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
mods []string
|
||||||
|
key string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{[]string{"Mod"}, "Q", "Mod+Q"},
|
||||||
|
{[]string{"Mod", "Shift"}, "F", "Mod+Shift+F"},
|
||||||
|
{[]string{"Ctrl", "Alt"}, "Delete", "Ctrl+Alt+Delete"},
|
||||||
|
{nil, "Print", "Print"},
|
||||||
|
{[]string{}, "XF86AudioMute", "XF86AudioMute"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.expected, func(t *testing.T) {
|
||||||
|
kb := &NiriKeyBinding{
|
||||||
|
Mods: tt.mods,
|
||||||
|
Key: tt.key,
|
||||||
|
}
|
||||||
|
result := provider.formatKey(kb)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("formatKey(%v) = %q, want %q", kb, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriDefaultConfigDir(t *testing.T) {
|
||||||
|
originalXDG := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
defer os.Setenv("XDG_CONFIG_HOME", originalXDG)
|
||||||
|
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", "/custom/config")
|
||||||
|
dir := defaultNiriConfigDir()
|
||||||
|
if dir != "/custom/config/niri" {
|
||||||
|
t.Errorf("With XDG_CONFIG_HOME set, got %q, want %q", dir, "/custom/config/niri")
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
dir = defaultNiriConfigDir()
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
expected := filepath.Join(home, ".config", "niri")
|
||||||
|
if dir != expected {
|
||||||
|
t.Errorf("Without XDG_CONFIG_HOME, got %q, want %q", dir, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateBindsContent(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
binds map[string]*overrideBind
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty binds",
|
||||||
|
binds: map[string]*overrideBind{},
|
||||||
|
expected: "binds {}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple spawn bind",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+T": {
|
||||||
|
Key: "Mod+T",
|
||||||
|
Action: "spawn kitty",
|
||||||
|
Description: "Open Terminal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spawn with multiple args",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+Space": {
|
||||||
|
Key: "Mod+Space",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`,
|
||||||
|
Description: "Application Launcher",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bind with allow-when-locked",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"XF86AudioMute": {
|
||||||
|
Key: "XF86AudioMute",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "audio" "mute"`,
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple action without args",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+Q": {
|
||||||
|
Key: "Mod+Q",
|
||||||
|
Action: "close-window",
|
||||||
|
Description: "Close Window",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+Q hotkey-overlay-title="Close Window" { close-window; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "recent-windows action",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Alt+Tab": {
|
||||||
|
Key: "Alt+Tab",
|
||||||
|
Action: "next-window",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
}
|
||||||
|
|
||||||
|
recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := provider.generateBindsContent(tt.binds)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"Mod+Space": {
|
||||||
|
Key: "Mod+Space",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`,
|
||||||
|
Description: "Application Launcher",
|
||||||
|
},
|
||||||
|
"XF86AudioMute": {
|
||||||
|
Key: "XF86AudioMute",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "audio" "mute"`,
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
"Mod+Q": {
|
||||||
|
Key: "Mod+Q",
|
||||||
|
Action: "close-window",
|
||||||
|
Description: "Close Window",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Errorf("Expected 3 keybinds after round-trip, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+Shift+Ctrl+D { debug-toggle-damage; }
|
||||||
|
Super+D { spawn "niri" "msg" "action" "toggle-overview"; }
|
||||||
|
Super+Tab repeat=false { toggle-overview; }
|
||||||
|
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||||
|
|
||||||
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
|
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||||
|
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||||
|
}
|
||||||
|
|
||||||
|
XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
||||||
|
}
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
||||||
|
}
|
||||||
|
|
||||||
|
Mod+Q repeat=false { close-window; }
|
||||||
|
Mod+F { maximize-column; }
|
||||||
|
Mod+Shift+F { fullscreen-window; }
|
||||||
|
|
||||||
|
Mod+Left { focus-column-left; }
|
||||||
|
Mod+Down { focus-window-down; }
|
||||||
|
Mod+Up { focus-window-up; }
|
||||||
|
Mod+Right { focus-column-right; }
|
||||||
|
|
||||||
|
Mod+1 { focus-workspace 1; }
|
||||||
|
Mod+2 { focus-workspace 2; }
|
||||||
|
Mod+Shift+1 { move-column-to-workspace 1; }
|
||||||
|
Mod+Shift+2 { move-column-to-workspace 2; }
|
||||||
|
|
||||||
|
Print { screenshot; }
|
||||||
|
Ctrl+Print { screenshot-screen; }
|
||||||
|
Alt+Print { screenshot-window; }
|
||||||
|
|
||||||
|
Mod+Shift+E { quit; }
|
||||||
|
}
|
||||||
|
|
||||||
|
recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
Alt+Shift+Tab { previous-window scope="output"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewNiriProvider(tmpDir)
|
||||||
|
cheatSheet, err := provider.GetCheatSheet()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBinds := 0
|
||||||
|
for _, binds := range cheatSheet.Binds {
|
||||||
|
totalBinds += len(binds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalBinds < 20 {
|
||||||
|
t.Errorf("Expected at least 20 keybinds, got %d", totalBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cheatSheet.Binds["Alt-Tab"]) < 2 {
|
||||||
|
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
binds map[string]*overrideBind
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "workspace with numeric arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+1": {
|
||||||
|
Key: "Mod+1",
|
||||||
|
Action: "focus-workspace 1",
|
||||||
|
Description: "Focus Workspace 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "workspace with large numeric arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+0": {
|
||||||
|
Key: "Mod+0",
|
||||||
|
Action: "focus-workspace 10",
|
||||||
|
Description: "Focus Workspace 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "percentage string arg (should be quoted)",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Super+Minus": {
|
||||||
|
Key: "Super+Minus",
|
||||||
|
Action: `set-column-width "-10%"`,
|
||||||
|
Description: "Adjust Column Width -10%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positive percentage string arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Super+Equal": {
|
||||||
|
Key: "Super+Equal",
|
||||||
|
Action: `set-column-width "+10%"`,
|
||||||
|
Description: "Adjust Column Width +10%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := provider.generateBindsContent(tt.binds)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"Super+Equal": {
|
||||||
|
Key: "Super+Equal",
|
||||||
|
Action: "set-window-height +10%",
|
||||||
|
Description: "Adjust Window Height +10%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
expected := `binds {
|
||||||
|
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if content != expected {
|
||||||
|
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"XF86AudioLowerVolume": {
|
||||||
|
Key: "XF86AudioLowerVolume",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "audio" "decrement" "3"`,
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
expected := `binds {
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if content != expected {
|
||||||
|
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"XF86AudioLowerVolume": {
|
||||||
|
Key: "XF86AudioLowerVolume",
|
||||||
|
Action: "spawn dms ipc call audio decrement 3",
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
expected := `binds {
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if content != expected {
|
||||||
|
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"Mod+1": {
|
||||||
|
Key: "Mod+1",
|
||||||
|
Action: "focus-workspace 1",
|
||||||
|
Description: "Focus Workspace 1",
|
||||||
|
},
|
||||||
|
"Mod+2": {
|
||||||
|
Key: "Mod+2",
|
||||||
|
Action: "focus-workspace 2",
|
||||||
|
Description: "Focus Workspace 2",
|
||||||
|
},
|
||||||
|
"Mod+Shift+1": {
|
||||||
|
Key: "Mod+Shift+1",
|
||||||
|
Action: "move-column-to-workspace 1",
|
||||||
|
Description: "Move to Workspace 1",
|
||||||
|
},
|
||||||
|
"Super+Minus": {
|
||||||
|
Key: "Super+Minus",
|
||||||
|
Action: "set-column-width -10%",
|
||||||
|
Description: "Adjust Column Width -10%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 4 {
|
||||||
|
t.Errorf("Expected 4 keybinds after round-trip, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundFocusWS1 := false
|
||||||
|
foundMoveWS1 := false
|
||||||
|
foundSetWidth := false
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch {
|
||||||
|
case kb.Action == "focus-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
|
||||||
|
foundFocusWS1 = true
|
||||||
|
case kb.Action == "move-column-to-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
|
||||||
|
foundMoveWS1 = true
|
||||||
|
case kb.Action == "set-column-width" && len(kb.Args) > 0 && kb.Args[0] == "-10%":
|
||||||
|
foundSetWidth = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundFocusWS1 {
|
||||||
|
t.Error("focus-workspace 1 not found after round-trip")
|
||||||
|
}
|
||||||
|
if !foundMoveWS1 {
|
||||||
|
t.Error("move-column-to-workspace 1 not found after round-trip")
|
||||||
|
}
|
||||||
|
if !foundSetWidth {
|
||||||
|
t.Error("set-column-width -10% not found after round-trip")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,6 +99,7 @@ func (s *SwayProvider) convertKeybind(kb *SwayKeyBinding, subcategory string) ke
|
|||||||
return keybinds.Keybind{
|
return keybinds.Keybind{
|
||||||
Key: key,
|
Key: key,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
|
Action: kb.Command,
|
||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,41 @@
|
|||||||
package keybinds
|
package keybinds
|
||||||
|
|
||||||
type Keybind struct {
|
type Keybind struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Description string `json:"desc"`
|
Description string `json:"desc"`
|
||||||
Subcategory string `json:"subcat,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
|
Subcategory string `json:"subcat,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Conflict *Keybind `json:"conflict,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DMSBindsStatus struct {
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
Included bool `json:"included"`
|
||||||
|
IncludePosition int `json:"includePosition"`
|
||||||
|
TotalIncludes int `json:"totalIncludes"`
|
||||||
|
BindsAfterDMS int `json:"bindsAfterDms"`
|
||||||
|
Effective bool `json:"effective"`
|
||||||
|
OverriddenBy int `json:"overriddenBy"`
|
||||||
|
StatusMessage string `json:"statusMessage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheatSheet struct {
|
type CheatSheet struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
Binds map[string][]Keybind `json:"binds"`
|
Binds map[string][]Keybind `json:"binds"`
|
||||||
|
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
|
||||||
|
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Name() string
|
Name() string
|
||||||
GetCheatSheet() (*CheatSheet, error)
|
GetCheatSheet() (*CheatSheet, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WritableProvider interface {
|
||||||
|
Provider
|
||||||
|
SetBind(key, action, description string, options map[string]any) error
|
||||||
|
RemoveBind(key string) error
|
||||||
|
GetOverridePath() string
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func (l *FileLogger) writeToFile(message string) {
|
|||||||
redacted := l.redactPassword(message)
|
redacted := l.redactPassword(message)
|
||||||
timestamp := time.Now().Format("15:04:05.000")
|
timestamp := time.Now().Format("15:04:05.000")
|
||||||
|
|
||||||
l.writer.WriteString(fmt.Sprintf("[%s] %s\n", timestamp, redacted)) //nolint:errcheck
|
fmt.Fprintf(l.writer, "[%s] %s\n", timestamp, redacted)
|
||||||
l.writer.Flush()
|
l.writer.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
type Logger struct{ *cblog.Logger }
|
type Logger struct{ *cblog.Logger }
|
||||||
|
|
||||||
// Printf routes goose/info-style logs through Infof.
|
// Printf routes goose/info-style logs through Infof.
|
||||||
func (l *Logger) Printf(format string, v ...interface{}) { l.Infof(format, v...) }
|
func (l *Logger) Printf(format string, v ...any) { l.Infof(format, v...) }
|
||||||
|
|
||||||
// Fatalf keeps goose’s contract of exiting the program.
|
// Fatalf keeps goose’s contract of exiting the program.
|
||||||
func (l *Logger) Fatalf(format string, v ...interface{}) { l.Logger.Fatalf(format, v...) }
|
func (l *Logger) Fatalf(format string, v ...any) { l.Logger.Fatalf(format, v...) }
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger *Logger
|
logger *Logger
|
||||||
@@ -104,13 +104,13 @@ func GetLogger() *Logger {
|
|||||||
|
|
||||||
// * Convenience wrappers
|
// * Convenience wrappers
|
||||||
|
|
||||||
func Debug(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Debug(msg, keyvals...) }
|
func Debug(msg any, keyvals ...any) { GetLogger().Debug(msg, keyvals...) }
|
||||||
func Debugf(format string, v ...interface{}) { GetLogger().Logger.Debugf(format, v...) }
|
func Debugf(format string, v ...any) { GetLogger().Debugf(format, v...) }
|
||||||
func Info(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Info(msg, keyvals...) }
|
func Info(msg any, keyvals ...any) { GetLogger().Info(msg, keyvals...) }
|
||||||
func Infof(format string, v ...interface{}) { GetLogger().Logger.Infof(format, v...) }
|
func Infof(format string, v ...any) { GetLogger().Infof(format, v...) }
|
||||||
func Warn(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Warn(msg, keyvals...) }
|
func Warn(msg any, keyvals ...any) { GetLogger().Warn(msg, keyvals...) }
|
||||||
func Warnf(format string, v ...interface{}) { GetLogger().Logger.Warnf(format, v...) }
|
func Warnf(format string, v ...any) { GetLogger().Warnf(format, v...) }
|
||||||
func Error(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Error(msg, keyvals...) }
|
func Error(msg any, keyvals ...any) { GetLogger().Error(msg, keyvals...) }
|
||||||
func Errorf(format string, v ...interface{}) { GetLogger().Logger.Errorf(format, v...) }
|
func Errorf(format string, v ...any) { GetLogger().Errorf(format, v...) }
|
||||||
func Fatal(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Fatal(msg, keyvals...) }
|
func Fatal(msg any, keyvals ...any) { GetLogger().Fatal(msg, keyvals...) }
|
||||||
func Fatalf(format string, v ...interface{}) { GetLogger().Logger.Fatalf(format, v...) }
|
func Fatalf(format string, v ...any) { GetLogger().Fatalf(format, v...) }
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ type MockDBusConn_Object_Call struct {
|
|||||||
// Object is a helper method to define mock.On call
|
// Object is a helper method to define mock.On call
|
||||||
// - dest string
|
// - dest string
|
||||||
// - path dbus.ObjectPath
|
// - path dbus.ObjectPath
|
||||||
func (_e *MockDBusConn_Expecter) Object(dest interface{}, path interface{}) *MockDBusConn_Object_Call {
|
func (_e *MockDBusConn_Expecter) Object(dest any, path any) *MockDBusConn_Object_Call {
|
||||||
return &MockDBusConn_Object_Call{Call: _e.mock.On("Object", dest, path)}
|
return &MockDBusConn_Object_Call{Call: _e.mock.On("Object", dest, path)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,8 @@ func (_c *MockDBusConn_Object_Call) RunAndReturn(run func(string, dbus.ObjectPat
|
|||||||
func NewMockDBusConn(t interface {
|
func NewMockDBusConn(t interface {
|
||||||
mock.TestingT
|
mock.TestingT
|
||||||
Cleanup(func())
|
Cleanup(func())
|
||||||
}) *MockDBusConn {
|
},
|
||||||
|
) *MockDBusConn {
|
||||||
mock := &MockDBusConn{}
|
mock := &MockDBusConn{}
|
||||||
mock.Mock.Test(t)
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ type MockGitClient_HasUpdates_Call struct {
|
|||||||
|
|
||||||
// HasUpdates is a helper method to define mock.On call
|
// HasUpdates is a helper method to define mock.On call
|
||||||
// - path string
|
// - path string
|
||||||
func (_e *MockGitClient_Expecter) HasUpdates(path interface{}) *MockGitClient_HasUpdates_Call {
|
func (_e *MockGitClient_Expecter) HasUpdates(path any) *MockGitClient_HasUpdates_Call {
|
||||||
return &MockGitClient_HasUpdates_Call{Call: _e.mock.On("HasUpdates", path)}
|
return &MockGitClient_HasUpdates_Call{Call: _e.mock.On("HasUpdates", path)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ type MockGitClient_PlainClone_Call struct {
|
|||||||
// PlainClone is a helper method to define mock.On call
|
// PlainClone is a helper method to define mock.On call
|
||||||
// - path string
|
// - path string
|
||||||
// - url string
|
// - url string
|
||||||
func (_e *MockGitClient_Expecter) PlainClone(path interface{}, url interface{}) *MockGitClient_PlainClone_Call {
|
func (_e *MockGitClient_Expecter) PlainClone(path any, url any) *MockGitClient_PlainClone_Call {
|
||||||
return &MockGitClient_PlainClone_Call{Call: _e.mock.On("PlainClone", path, url)}
|
return &MockGitClient_PlainClone_Call{Call: _e.mock.On("PlainClone", path, url)}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ type MockGitClient_Pull_Call struct {
|
|||||||
|
|
||||||
// Pull is a helper method to define mock.On call
|
// Pull is a helper method to define mock.On call
|
||||||
// - path string
|
// - path string
|
||||||
func (_e *MockGitClient_Expecter) Pull(path interface{}) *MockGitClient_Pull_Call {
|
func (_e *MockGitClient_Expecter) Pull(path any) *MockGitClient_Pull_Call {
|
||||||
return &MockGitClient_Pull_Call{Call: _e.mock.On("Pull", path)}
|
return &MockGitClient_Pull_Call{Call: _e.mock.On("Pull", path)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : internal/proto/xml/keyboard-shortcuts-inhibit-unstable-v1.xml
|
||||||
|
//
|
||||||
|
// keyboard_shortcuts_inhibit_unstable_v1 Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2017 Red Hat Inc.
|
||||||
|
//
|
||||||
|
// 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 keyboard_shortcuts_inhibit
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1InterfaceName 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 ZwpKeyboardShortcutsInhibitManagerV1InterfaceName = "zwp_keyboard_shortcuts_inhibit_manager_v1"
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
|
||||||
|
//
|
||||||
|
// A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||||
|
type ZwpKeyboardShortcutsInhibitManagerV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
|
||||||
|
//
|
||||||
|
// A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||||
|
func NewZwpKeyboardShortcutsInhibitManagerV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitManagerV1 {
|
||||||
|
zwpKeyboardShortcutsInhibitManagerV1 := &ZwpKeyboardShortcutsInhibitManagerV1{}
|
||||||
|
ctx.Register(zwpKeyboardShortcutsInhibitManagerV1)
|
||||||
|
return zwpKeyboardShortcutsInhibitManagerV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the keyboard shortcuts inhibitor object
|
||||||
|
//
|
||||||
|
// Destroy the keyboard shortcuts inhibitor manager.
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitManagerV1) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 0
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// InhibitShortcuts : create a new keyboard shortcuts inhibitor object
|
||||||
|
//
|
||||||
|
// Create a new keyboard shortcuts inhibitor object associated with
|
||||||
|
// the given surface for the given seat.
|
||||||
|
//
|
||||||
|
// If shortcuts are already inhibited for the specified seat and surface,
|
||||||
|
// a protocol error "already_inhibited" is raised by the compositor.
|
||||||
|
//
|
||||||
|
// surface: the surface that inhibits the keyboard shortcuts behavior
|
||||||
|
// seat: the wl_seat for which keyboard shortcuts should be disabled
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitManagerV1) InhibitShortcuts(surface *client.Surface, seat *client.Seat) (*ZwpKeyboardShortcutsInhibitorV1, error) {
|
||||||
|
id := NewZwpKeyboardShortcutsInhibitorV1(i.Context())
|
||||||
|
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], id.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], seat.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwpKeyboardShortcutsInhibitManagerV1Error uint32
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1Error :
|
||||||
|
const (
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited : the shortcuts are already inhibited for this surface
|
||||||
|
ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited ZwpKeyboardShortcutsInhibitManagerV1Error = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
|
||||||
|
return "already_inhibited"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
|
||||||
|
return "0"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1InterfaceName 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 ZwpKeyboardShortcutsInhibitorV1InterfaceName = "zwp_keyboard_shortcuts_inhibitor_v1"
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
|
||||||
|
//
|
||||||
|
// A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||||
|
// its own keyboard shortcuts when the associated surface has keyboard
|
||||||
|
// focus. As a result, when the surface has keyboard focus on the given
|
||||||
|
// seat, it will receive all key events originating from the specified
|
||||||
|
// seat, even those which would normally be caught by the compositor for
|
||||||
|
// its own shortcuts.
|
||||||
|
//
|
||||||
|
// The Wayland compositor is however under no obligation to disable
|
||||||
|
// all of its shortcuts, and may keep some special key combo for its own
|
||||||
|
// use, including but not limited to one allowing the user to forcibly
|
||||||
|
// restore normal keyboard events routing in the case of an unwilling
|
||||||
|
// client. The compositor may also use the same key combo to reactivate
|
||||||
|
// an existing shortcut inhibitor that was previously deactivated on
|
||||||
|
// user request.
|
||||||
|
//
|
||||||
|
// When the compositor restores its own keyboard shortcuts, an
|
||||||
|
// "inactive" event is emitted to notify the client that the keyboard
|
||||||
|
// shortcuts inhibitor is not effectively active for the surface and
|
||||||
|
// seat any more, and the client should not expect to receive all
|
||||||
|
// keyboard events.
|
||||||
|
//
|
||||||
|
// When the keyboard shortcuts inhibitor is inactive, the client has
|
||||||
|
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||||
|
//
|
||||||
|
// The user can chose to re-enable a previously deactivated keyboard
|
||||||
|
// shortcuts inhibitor using any mechanism the compositor may offer,
|
||||||
|
// in which case the compositor will send an "active" event to notify
|
||||||
|
// the client.
|
||||||
|
//
|
||||||
|
// If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||||
|
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||||
|
// compositor will restore its own keyboard shortcuts but no "inactive"
|
||||||
|
// event is emitted in this case.
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
activeHandler ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc
|
||||||
|
inactiveHandler ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
|
||||||
|
//
|
||||||
|
// A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||||
|
// its own keyboard shortcuts when the associated surface has keyboard
|
||||||
|
// focus. As a result, when the surface has keyboard focus on the given
|
||||||
|
// seat, it will receive all key events originating from the specified
|
||||||
|
// seat, even those which would normally be caught by the compositor for
|
||||||
|
// its own shortcuts.
|
||||||
|
//
|
||||||
|
// The Wayland compositor is however under no obligation to disable
|
||||||
|
// all of its shortcuts, and may keep some special key combo for its own
|
||||||
|
// use, including but not limited to one allowing the user to forcibly
|
||||||
|
// restore normal keyboard events routing in the case of an unwilling
|
||||||
|
// client. The compositor may also use the same key combo to reactivate
|
||||||
|
// an existing shortcut inhibitor that was previously deactivated on
|
||||||
|
// user request.
|
||||||
|
//
|
||||||
|
// When the compositor restores its own keyboard shortcuts, an
|
||||||
|
// "inactive" event is emitted to notify the client that the keyboard
|
||||||
|
// shortcuts inhibitor is not effectively active for the surface and
|
||||||
|
// seat any more, and the client should not expect to receive all
|
||||||
|
// keyboard events.
|
||||||
|
//
|
||||||
|
// When the keyboard shortcuts inhibitor is inactive, the client has
|
||||||
|
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||||
|
//
|
||||||
|
// The user can chose to re-enable a previously deactivated keyboard
|
||||||
|
// shortcuts inhibitor using any mechanism the compositor may offer,
|
||||||
|
// in which case the compositor will send an "active" event to notify
|
||||||
|
// the client.
|
||||||
|
//
|
||||||
|
// If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||||
|
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||||
|
// compositor will restore its own keyboard shortcuts but no "inactive"
|
||||||
|
// event is emitted in this case.
|
||||||
|
func NewZwpKeyboardShortcutsInhibitorV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitorV1 {
|
||||||
|
zwpKeyboardShortcutsInhibitorV1 := &ZwpKeyboardShortcutsInhibitorV1{}
|
||||||
|
ctx.Register(zwpKeyboardShortcutsInhibitorV1)
|
||||||
|
return zwpKeyboardShortcutsInhibitorV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the keyboard shortcuts inhibitor object
|
||||||
|
//
|
||||||
|
// Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 0
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1ActiveEvent : shortcuts are inhibited
|
||||||
|
//
|
||||||
|
// This event indicates that the shortcut inhibitor is active.
|
||||||
|
//
|
||||||
|
// The compositor sends this event every time compositor shortcuts
|
||||||
|
// are inhibited on behalf of the surface. When active, the client
|
||||||
|
// may receive input events normally reserved by the compositor
|
||||||
|
// (see zwp_keyboard_shortcuts_inhibitor_v1).
|
||||||
|
//
|
||||||
|
// This occurs typically when the initial request "inhibit_shortcuts"
|
||||||
|
// first becomes active or when the user instructs the compositor to
|
||||||
|
// re-enable and existing shortcuts inhibitor using any mechanism
|
||||||
|
// offered by the compositor.
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1ActiveEvent struct{}
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1ActiveEvent)
|
||||||
|
|
||||||
|
// SetActiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1ActiveEvent
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) SetActiveHandler(f ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc) {
|
||||||
|
i.activeHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1InactiveEvent : shortcuts are restored
|
||||||
|
//
|
||||||
|
// This event indicates that the shortcuts inhibitor is inactive,
|
||||||
|
// normal shortcuts processing is restored by the compositor.
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1InactiveEvent struct{}
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1InactiveEvent)
|
||||||
|
|
||||||
|
// SetInactiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1InactiveEvent
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) SetInactiveHandler(f ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc) {
|
||||||
|
i.inactiveHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||||
|
switch opcode {
|
||||||
|
case 0:
|
||||||
|
if i.activeHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwpKeyboardShortcutsInhibitorV1ActiveEvent
|
||||||
|
|
||||||
|
i.activeHandler(e)
|
||||||
|
case 1:
|
||||||
|
if i.inactiveHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwpKeyboardShortcutsInhibitorV1InactiveEvent
|
||||||
|
|
||||||
|
i.inactiveHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
792
core/internal/proto/wlr_layer_shell/layer_shell.go
Normal file
792
core/internal/proto/wlr_layer_shell/layer_shell.go
Normal file
@@ -0,0 +1,792 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : internal/proto/xml/wlr-layer-shell-unstable-v1.xml
|
||||||
|
//
|
||||||
|
// wlr_layer_shell_unstable_v1 Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2017 Drew DeVault
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, distribute, and sell this
|
||||||
|
// software and its documentation for any purpose is hereby granted
|
||||||
|
// without fee, provided that the above copyright notice appear in
|
||||||
|
// all copies and that both that copyright notice and this permission
|
||||||
|
// notice appear in supporting documentation, and that the name of
|
||||||
|
// the copyright holders not be used in advertising or publicity
|
||||||
|
// pertaining to distribution of the software without specific,
|
||||||
|
// written prior permission. The copyright holders make no
|
||||||
|
// representations about the suitability of this software for any
|
||||||
|
// purpose. It is provided "as is" without express or implied
|
||||||
|
// warranty.
|
||||||
|
//
|
||||||
|
// THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||||
|
// SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
// FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||||
|
// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||||
|
// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||||
|
// THIS SOFTWARE.
|
||||||
|
|
||||||
|
package wlr_layer_shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
xdg_shell "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/stable/xdg-shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1InterfaceName 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 ZwlrLayerShellV1InterfaceName = "zwlr_layer_shell_v1"
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1 : create surfaces that are layers of the desktop
|
||||||
|
//
|
||||||
|
// Clients can use this interface to assign the surface_layer role to
|
||||||
|
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||||
|
// rendered with a defined z-depth respective to each other. They may also be
|
||||||
|
// anchored to the edges and corners of a screen and specify input handling
|
||||||
|
// semantics. This interface should be suitable for the implementation of
|
||||||
|
// many desktop shell components, and a broad number of other applications
|
||||||
|
// that interact with the desktop.
|
||||||
|
type ZwlrLayerShellV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrLayerShellV1 : create surfaces that are layers of the desktop
|
||||||
|
//
|
||||||
|
// Clients can use this interface to assign the surface_layer role to
|
||||||
|
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||||
|
// rendered with a defined z-depth respective to each other. They may also be
|
||||||
|
// anchored to the edges and corners of a screen and specify input handling
|
||||||
|
// semantics. This interface should be suitable for the implementation of
|
||||||
|
// many desktop shell components, and a broad number of other applications
|
||||||
|
// that interact with the desktop.
|
||||||
|
func NewZwlrLayerShellV1(ctx *client.Context) *ZwlrLayerShellV1 {
|
||||||
|
zwlrLayerShellV1 := &ZwlrLayerShellV1{}
|
||||||
|
ctx.Register(zwlrLayerShellV1)
|
||||||
|
return zwlrLayerShellV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLayerSurface : create a layer_surface from a surface
|
||||||
|
//
|
||||||
|
// Create a layer surface for an existing surface. This assigns the role of
|
||||||
|
// layer_surface, or raises a protocol error if another role is already
|
||||||
|
// assigned.
|
||||||
|
//
|
||||||
|
// Creating a layer surface from a wl_surface which has a buffer attached
|
||||||
|
// or committed is a client error, and any attempts by a client to attach
|
||||||
|
// or manipulate a buffer prior to the first layer_surface.configure call
|
||||||
|
// must also be treated as errors.
|
||||||
|
//
|
||||||
|
// After creating a layer_surface object and setting it up, the client
|
||||||
|
// must perform an initial commit without any buffer attached.
|
||||||
|
// The compositor will reply with a layer_surface.configure event.
|
||||||
|
// The client must acknowledge it and is then allowed to attach a buffer
|
||||||
|
// to map the surface.
|
||||||
|
//
|
||||||
|
// You may pass NULL for output to allow the compositor to decide which
|
||||||
|
// output to use. Generally this will be the one that the user most
|
||||||
|
// recently interacted with.
|
||||||
|
//
|
||||||
|
// Clients can specify a namespace that defines the purpose of the layer
|
||||||
|
// surface.
|
||||||
|
//
|
||||||
|
// layer: layer to add this surface to
|
||||||
|
// namespace: namespace for the layer surface
|
||||||
|
func (i *ZwlrLayerShellV1) GetLayerSurface(surface *client.Surface, output *client.Output, layer uint32, namespace string) (*ZwlrLayerSurfaceV1, error) {
|
||||||
|
id := NewZwlrLayerSurfaceV1(i.Context())
|
||||||
|
const opcode = 0
|
||||||
|
namespaceLen := client.PaddedLen(len(namespace) + 1)
|
||||||
|
_reqBufLen := 8 + 4 + 4 + 4 + 4 + (4 + namespaceLen)
|
||||||
|
_reqBuf := make([]byte, _reqBufLen)
|
||||||
|
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], id.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||||
|
l += 4
|
||||||
|
if output == nil {
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], 0)
|
||||||
|
l += 4
|
||||||
|
} else {
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||||
|
l += 4
|
||||||
|
}
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(layer))
|
||||||
|
l += 4
|
||||||
|
client.PutString(_reqBuf[l:l+(4+namespaceLen)], namespace)
|
||||||
|
l += (4 + namespaceLen)
|
||||||
|
err := i.Context().WriteMsg(_reqBuf, nil)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the layer_shell object
|
||||||
|
//
|
||||||
|
// This request indicates that the client will not use the layer_shell
|
||||||
|
// object any more. Objects that have been created through this instance
|
||||||
|
// are not affected.
|
||||||
|
func (i *ZwlrLayerShellV1) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 1
|
||||||
|
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 ZwlrLayerShellV1Error uint32
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1Error :
|
||||||
|
const (
|
||||||
|
// ZwlrLayerShellV1ErrorRole : wl_surface has another role
|
||||||
|
ZwlrLayerShellV1ErrorRole ZwlrLayerShellV1Error = 0
|
||||||
|
// ZwlrLayerShellV1ErrorInvalidLayer : layer value is invalid
|
||||||
|
ZwlrLayerShellV1ErrorInvalidLayer ZwlrLayerShellV1Error = 1
|
||||||
|
// ZwlrLayerShellV1ErrorAlreadyConstructed : wl_surface has a buffer attached or committed
|
||||||
|
ZwlrLayerShellV1ErrorAlreadyConstructed ZwlrLayerShellV1Error = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1ErrorRole:
|
||||||
|
return "role"
|
||||||
|
case ZwlrLayerShellV1ErrorInvalidLayer:
|
||||||
|
return "invalid_layer"
|
||||||
|
case ZwlrLayerShellV1ErrorAlreadyConstructed:
|
||||||
|
return "already_constructed"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1ErrorRole:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerShellV1ErrorInvalidLayer:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerShellV1ErrorAlreadyConstructed:
|
||||||
|
return "2"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerShellV1Layer uint32
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1Layer : available layers for surfaces
|
||||||
|
//
|
||||||
|
// These values indicate which layers a surface can be rendered in. They
|
||||||
|
// are ordered by z depth, bottom-most first. Traditional shell surfaces
|
||||||
|
// will typically be rendered between the bottom and top layers.
|
||||||
|
// Fullscreen shell surfaces are typically rendered at the top layer.
|
||||||
|
// Multiple surfaces can share a single layer, and ordering within a
|
||||||
|
// single layer is undefined.
|
||||||
|
const (
|
||||||
|
ZwlrLayerShellV1LayerBackground ZwlrLayerShellV1Layer = 0
|
||||||
|
ZwlrLayerShellV1LayerBottom ZwlrLayerShellV1Layer = 1
|
||||||
|
ZwlrLayerShellV1LayerTop ZwlrLayerShellV1Layer = 2
|
||||||
|
ZwlrLayerShellV1LayerOverlay ZwlrLayerShellV1Layer = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Layer) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1LayerBackground:
|
||||||
|
return "background"
|
||||||
|
case ZwlrLayerShellV1LayerBottom:
|
||||||
|
return "bottom"
|
||||||
|
case ZwlrLayerShellV1LayerTop:
|
||||||
|
return "top"
|
||||||
|
case ZwlrLayerShellV1LayerOverlay:
|
||||||
|
return "overlay"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Layer) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1LayerBackground:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerShellV1LayerBottom:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerShellV1LayerTop:
|
||||||
|
return "2"
|
||||||
|
case ZwlrLayerShellV1LayerOverlay:
|
||||||
|
return "3"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Layer) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1InterfaceName 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 ZwlrLayerSurfaceV1InterfaceName = "zwlr_layer_surface_v1"
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1 : layer metadata interface
|
||||||
|
//
|
||||||
|
// An interface that may be implemented by a wl_surface, for surfaces that
|
||||||
|
// are designed to be rendered as a layer of a stacked desktop-like
|
||||||
|
// environment.
|
||||||
|
//
|
||||||
|
// Layer surface state (layer, size, anchor, exclusive zone,
|
||||||
|
// margin, interactivity) is double-buffered, and will be applied at the
|
||||||
|
// time wl_surface.commit of the corresponding wl_surface is called.
|
||||||
|
//
|
||||||
|
// Attaching a null buffer to a layer surface unmaps it.
|
||||||
|
//
|
||||||
|
// Unmapping a layer_surface means that the surface cannot be shown by the
|
||||||
|
// compositor until it is explicitly mapped again. The layer_surface
|
||||||
|
// returns to the state it had right after layer_shell.get_layer_surface.
|
||||||
|
// The client can re-map the surface by performing a commit without any
|
||||||
|
// buffer attached, waiting for a configure event and handling it as usual.
|
||||||
|
type ZwlrLayerSurfaceV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
configureHandler ZwlrLayerSurfaceV1ConfigureHandlerFunc
|
||||||
|
closedHandler ZwlrLayerSurfaceV1ClosedHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrLayerSurfaceV1 : layer metadata interface
|
||||||
|
//
|
||||||
|
// An interface that may be implemented by a wl_surface, for surfaces that
|
||||||
|
// are designed to be rendered as a layer of a stacked desktop-like
|
||||||
|
// environment.
|
||||||
|
//
|
||||||
|
// Layer surface state (layer, size, anchor, exclusive zone,
|
||||||
|
// margin, interactivity) is double-buffered, and will be applied at the
|
||||||
|
// time wl_surface.commit of the corresponding wl_surface is called.
|
||||||
|
//
|
||||||
|
// Attaching a null buffer to a layer surface unmaps it.
|
||||||
|
//
|
||||||
|
// Unmapping a layer_surface means that the surface cannot be shown by the
|
||||||
|
// compositor until it is explicitly mapped again. The layer_surface
|
||||||
|
// returns to the state it had right after layer_shell.get_layer_surface.
|
||||||
|
// The client can re-map the surface by performing a commit without any
|
||||||
|
// buffer attached, waiting for a configure event and handling it as usual.
|
||||||
|
func NewZwlrLayerSurfaceV1(ctx *client.Context) *ZwlrLayerSurfaceV1 {
|
||||||
|
zwlrLayerSurfaceV1 := &ZwlrLayerSurfaceV1{}
|
||||||
|
ctx.Register(zwlrLayerSurfaceV1)
|
||||||
|
return zwlrLayerSurfaceV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize : sets the size of the surface
|
||||||
|
//
|
||||||
|
// Sets the size of the surface in surface-local coordinates. The
|
||||||
|
// compositor will display the surface centered with respect to its
|
||||||
|
// anchors.
|
||||||
|
//
|
||||||
|
// If you pass 0 for either value, the compositor will assign it and
|
||||||
|
// inform you of the assignment in the configure event. You must set your
|
||||||
|
// anchor to opposite edges in the dimensions you omit; not doing so is a
|
||||||
|
// protocol error. Both values are 0 by default.
|
||||||
|
//
|
||||||
|
// Size is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetSize(width, height 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(width))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAnchor : configures the anchor point of the surface
|
||||||
|
//
|
||||||
|
// Requests that the compositor anchor the surface to the specified edges
|
||||||
|
// and corners. If two orthogonal edges are specified (e.g. 'top' and
|
||||||
|
// 'left'), then the anchor point will be the intersection of the edges
|
||||||
|
// (e.g. the top left corner of the output); otherwise the anchor point
|
||||||
|
// will be centered on that edge, or in the center if none is specified.
|
||||||
|
//
|
||||||
|
// Anchor is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetAnchor(anchor uint32) error {
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 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(anchor))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExclusiveZone : configures the exclusive geometry of this surface
|
||||||
|
//
|
||||||
|
// Requests that the compositor avoids occluding an area with other
|
||||||
|
// surfaces. The compositor's use of this information is
|
||||||
|
// implementation-dependent - do not assume that this region will not
|
||||||
|
// actually be occluded.
|
||||||
|
//
|
||||||
|
// A positive value is only meaningful if the surface is anchored to one
|
||||||
|
// edge or an edge and both perpendicular edges. If the surface is not
|
||||||
|
// anchored, anchored to only two perpendicular edges (a corner), anchored
|
||||||
|
// to only two parallel edges or anchored to all edges, a positive value
|
||||||
|
// will be treated the same as zero.
|
||||||
|
//
|
||||||
|
// A positive zone is the distance from the edge in surface-local
|
||||||
|
// coordinates to consider exclusive.
|
||||||
|
//
|
||||||
|
// Surfaces that do not wish to have an exclusive zone may instead specify
|
||||||
|
// how they should interact with surfaces that do. If set to zero, the
|
||||||
|
// surface indicates that it would like to be moved to avoid occluding
|
||||||
|
// surfaces with a positive exclusive zone. If set to -1, the surface
|
||||||
|
// indicates that it would not like to be moved to accommodate for other
|
||||||
|
// surfaces, and the compositor should extend it all the way to the edges
|
||||||
|
// it is anchored to.
|
||||||
|
//
|
||||||
|
// For example, a panel might set its exclusive zone to 10, so that
|
||||||
|
// maximized shell surfaces are not shown on top of it. A notification
|
||||||
|
// might set its exclusive zone to 0, so that it is moved to avoid
|
||||||
|
// occluding the panel, but shell surfaces are shown underneath it. A
|
||||||
|
// wallpaper or lock screen might set their exclusive zone to -1, so that
|
||||||
|
// they stretch below or over the panel.
|
||||||
|
//
|
||||||
|
// The default value is 0.
|
||||||
|
//
|
||||||
|
// Exclusive zone is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetExclusiveZone(zone int32) error {
|
||||||
|
const opcode = 2
|
||||||
|
const _reqBufLen = 8 + 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(zone))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMargin : sets a margin from the anchor point
|
||||||
|
//
|
||||||
|
// Requests that the surface be placed some distance away from the anchor
|
||||||
|
// point on the output, in surface-local coordinates. Setting this value
|
||||||
|
// for edges you are not anchored to has no effect.
|
||||||
|
//
|
||||||
|
// The exclusive zone includes the margin.
|
||||||
|
//
|
||||||
|
// Margin is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetMargin(top, right, bottom, left int32) error {
|
||||||
|
const opcode = 3
|
||||||
|
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(top))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(right))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(bottom))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(left))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyboardInteractivity : requests keyboard events
|
||||||
|
//
|
||||||
|
// Set how keyboard events are delivered to this surface. By default,
|
||||||
|
// layer shell surfaces do not receive keyboard events; this request can
|
||||||
|
// be used to change this.
|
||||||
|
//
|
||||||
|
// This setting is inherited by child surfaces set by the get_popup
|
||||||
|
// request.
|
||||||
|
//
|
||||||
|
// Layer surfaces receive pointer, touch, and tablet events normally. If
|
||||||
|
// you do not want to receive them, set the input region on your surface
|
||||||
|
// to an empty region.
|
||||||
|
//
|
||||||
|
// Keyboard interactivity is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetKeyboardInteractivity(keyboardInteractivity uint32) error {
|
||||||
|
const opcode = 4
|
||||||
|
const _reqBufLen = 8 + 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(keyboardInteractivity))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPopup : assign this layer_surface as an xdg_popup parent
|
||||||
|
//
|
||||||
|
// This assigns an xdg_popup's parent to this layer_surface. This popup
|
||||||
|
// should have been created via xdg_surface::get_popup with the parent set
|
||||||
|
// to NULL, and this request must be invoked before committing the popup's
|
||||||
|
// initial state.
|
||||||
|
//
|
||||||
|
// See the documentation of xdg_popup for more details about what an
|
||||||
|
// xdg_popup is and how it is used.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) GetPopup(popup *xdg_shell.Popup) error {
|
||||||
|
const opcode = 5
|
||||||
|
const _reqBufLen = 8 + 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], popup.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AckConfigure : ack a configure event
|
||||||
|
//
|
||||||
|
// When a configure event is received, if a client commits the
|
||||||
|
// surface in response to the configure event, then the client
|
||||||
|
// must make an ack_configure request sometime before the commit
|
||||||
|
// request, passing along the serial of the configure event.
|
||||||
|
//
|
||||||
|
// If the client receives multiple configure events before it
|
||||||
|
// can respond to one, it only has to ack the last configure event.
|
||||||
|
//
|
||||||
|
// A client is not required to commit immediately after sending
|
||||||
|
// an ack_configure request - it may even ack_configure several times
|
||||||
|
// before its next surface commit.
|
||||||
|
//
|
||||||
|
// A client may send multiple ack_configure requests before committing, but
|
||||||
|
// only the last request sent before a commit indicates which configure
|
||||||
|
// event the client really is responding to.
|
||||||
|
//
|
||||||
|
// serial: the serial from the configure event
|
||||||
|
func (i *ZwlrLayerSurfaceV1) AckConfigure(serial uint32) error {
|
||||||
|
const opcode = 6
|
||||||
|
const _reqBufLen = 8 + 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(serial))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the layer_surface
|
||||||
|
//
|
||||||
|
// This request destroys the layer surface.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 7
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLayer : change the layer of the surface
|
||||||
|
//
|
||||||
|
// Change the layer that the surface is rendered on.
|
||||||
|
//
|
||||||
|
// Layer is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// layer: layer to move this surface to
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetLayer(layer uint32) error {
|
||||||
|
const opcode = 8
|
||||||
|
const _reqBufLen = 8 + 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(layer))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExclusiveEdge : set the edge the exclusive zone will be applied to
|
||||||
|
//
|
||||||
|
// Requests an edge for the exclusive zone to apply. The exclusive
|
||||||
|
// edge will be automatically deduced from anchor points when possible,
|
||||||
|
// but when the surface is anchored to a corner, it will be necessary
|
||||||
|
// to set it explicitly to disambiguate, as it is not possible to deduce
|
||||||
|
// which one of the two corner edges should be used.
|
||||||
|
//
|
||||||
|
// The edge must be one the surface is anchored to, otherwise the
|
||||||
|
// invalid_exclusive_edge protocol error will be raised.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetExclusiveEdge(edge uint32) error {
|
||||||
|
const opcode = 9
|
||||||
|
const _reqBufLen = 8 + 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(edge))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerSurfaceV1KeyboardInteractivity uint32
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1KeyboardInteractivity : types of keyboard interaction possible for a layer shell surface
|
||||||
|
//
|
||||||
|
// Types of keyboard interaction possible for layer shell surfaces. The
|
||||||
|
// rationale for this is twofold: (1) some applications are not interested
|
||||||
|
// in keyboard events and not allowing them to be focused can improve the
|
||||||
|
// desktop experience; (2) some applications will want to take exclusive
|
||||||
|
// keyboard focus.
|
||||||
|
const (
|
||||||
|
ZwlrLayerSurfaceV1KeyboardInteractivityNone ZwlrLayerSurfaceV1KeyboardInteractivity = 0
|
||||||
|
ZwlrLayerSurfaceV1KeyboardInteractivityExclusive ZwlrLayerSurfaceV1KeyboardInteractivity = 1
|
||||||
|
ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand ZwlrLayerSurfaceV1KeyboardInteractivity = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
|
||||||
|
return "none"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
|
||||||
|
return "exclusive"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
|
||||||
|
return "on_demand"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
|
||||||
|
return "2"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerSurfaceV1Error uint32
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1Error :
|
||||||
|
const (
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidSurfaceState : provided surface state is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidSurfaceState ZwlrLayerSurfaceV1Error = 0
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidSize : size is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidSize ZwlrLayerSurfaceV1Error = 1
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidAnchor : anchor bitfield is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidAnchor ZwlrLayerSurfaceV1Error = 2
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity : keyboard interactivity is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity ZwlrLayerSurfaceV1Error = 3
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge : exclusive edge is invalid given the surface anchors
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge ZwlrLayerSurfaceV1Error = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
|
||||||
|
return "invalid_surface_state"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSize:
|
||||||
|
return "invalid_size"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
|
||||||
|
return "invalid_anchor"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
|
||||||
|
return "invalid_keyboard_interactivity"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
|
||||||
|
return "invalid_exclusive_edge"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSize:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
|
||||||
|
return "2"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
|
||||||
|
return "3"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
|
||||||
|
return "4"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerSurfaceV1Anchor uint32
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1Anchor :
|
||||||
|
const (
|
||||||
|
// ZwlrLayerSurfaceV1AnchorTop : the top edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorTop ZwlrLayerSurfaceV1Anchor = 1
|
||||||
|
// ZwlrLayerSurfaceV1AnchorBottom : the bottom edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorBottom ZwlrLayerSurfaceV1Anchor = 2
|
||||||
|
// ZwlrLayerSurfaceV1AnchorLeft : the left edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorLeft ZwlrLayerSurfaceV1Anchor = 4
|
||||||
|
// ZwlrLayerSurfaceV1AnchorRight : the right edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorRight ZwlrLayerSurfaceV1Anchor = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Anchor) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1AnchorTop:
|
||||||
|
return "top"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorBottom:
|
||||||
|
return "bottom"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorLeft:
|
||||||
|
return "left"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorRight:
|
||||||
|
return "right"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Anchor) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1AnchorTop:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorBottom:
|
||||||
|
return "2"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorLeft:
|
||||||
|
return "4"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorRight:
|
||||||
|
return "8"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Anchor) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1ConfigureEvent : suggest a surface change
|
||||||
|
//
|
||||||
|
// The configure event asks the client to resize its surface.
|
||||||
|
//
|
||||||
|
// Clients should arrange their surface for the new states, and then send
|
||||||
|
// an ack_configure request with the serial sent in this configure event at
|
||||||
|
// some point before committing the new surface.
|
||||||
|
//
|
||||||
|
// The client is free to dismiss all but the last configure event it
|
||||||
|
// received.
|
||||||
|
//
|
||||||
|
// The width and height arguments specify the size of the window in
|
||||||
|
// surface-local coordinates.
|
||||||
|
//
|
||||||
|
// The size is a hint, in the sense that the client is free to ignore it if
|
||||||
|
// it doesn't resize, pick a smaller size (to satisfy aspect ratio or
|
||||||
|
// resize in steps of NxM pixels). If the client picks a smaller size and
|
||||||
|
// is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
|
||||||
|
// surface will be centered on this axis.
|
||||||
|
//
|
||||||
|
// If the width or height arguments are zero, it means the client should
|
||||||
|
// decide its own window dimension.
|
||||||
|
type ZwlrLayerSurfaceV1ConfigureEvent struct {
|
||||||
|
Serial uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
}
|
||||||
|
type ZwlrLayerSurfaceV1ConfigureHandlerFunc func(ZwlrLayerSurfaceV1ConfigureEvent)
|
||||||
|
|
||||||
|
// SetConfigureHandler : sets handler for ZwlrLayerSurfaceV1ConfigureEvent
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetConfigureHandler(f ZwlrLayerSurfaceV1ConfigureHandlerFunc) {
|
||||||
|
i.configureHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1ClosedEvent : surface should be closed
|
||||||
|
//
|
||||||
|
// The closed event is sent by the compositor when the surface will no
|
||||||
|
// longer be shown. The output may have been destroyed or the user may
|
||||||
|
// have asked for it to be removed. Further changes to the surface will be
|
||||||
|
// ignored. The client should destroy the resource after receiving this
|
||||||
|
// event, and create a new surface if they so choose.
|
||||||
|
type ZwlrLayerSurfaceV1ClosedEvent struct{}
|
||||||
|
type ZwlrLayerSurfaceV1ClosedHandlerFunc func(ZwlrLayerSurfaceV1ClosedEvent)
|
||||||
|
|
||||||
|
// SetClosedHandler : sets handler for ZwlrLayerSurfaceV1ClosedEvent
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetClosedHandler(f ZwlrLayerSurfaceV1ClosedHandlerFunc) {
|
||||||
|
i.closedHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ZwlrLayerSurfaceV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||||
|
switch opcode {
|
||||||
|
case 0:
|
||||||
|
if i.configureHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrLayerSurfaceV1ConfigureEvent
|
||||||
|
l := 0
|
||||||
|
e.Serial = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.configureHandler(e)
|
||||||
|
case 1:
|
||||||
|
if i.closedHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrLayerSurfaceV1ClosedEvent
|
||||||
|
|
||||||
|
i.closedHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
532
core/internal/proto/wlr_screencopy/screencopy.go
Normal file
532
core/internal/proto/wlr_screencopy/screencopy.go
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : internal/proto/xml/wlr-screencopy-unstable-v1.xml
|
||||||
|
//
|
||||||
|
// wlr_screencopy_unstable_v1 Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2018 Simon Ser
|
||||||
|
// Copyright © 2019 Andri Yngvason
|
||||||
|
//
|
||||||
|
// 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 wlr_screencopy
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
|
||||||
|
// ZwlrScreencopyManagerV1InterfaceName 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 ZwlrScreencopyManagerV1InterfaceName = "zwlr_screencopy_manager_v1"
|
||||||
|
|
||||||
|
// ZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
|
||||||
|
//
|
||||||
|
// This object is a manager which offers requests to start capturing from a
|
||||||
|
// source.
|
||||||
|
type ZwlrScreencopyManagerV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
|
||||||
|
//
|
||||||
|
// This object is a manager which offers requests to start capturing from a
|
||||||
|
// source.
|
||||||
|
func NewZwlrScreencopyManagerV1(ctx *client.Context) *ZwlrScreencopyManagerV1 {
|
||||||
|
zwlrScreencopyManagerV1 := &ZwlrScreencopyManagerV1{}
|
||||||
|
ctx.Register(zwlrScreencopyManagerV1)
|
||||||
|
return zwlrScreencopyManagerV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureOutput : capture an output
|
||||||
|
//
|
||||||
|
// Capture the next frame of an entire output.
|
||||||
|
//
|
||||||
|
// overlayCursor: composite cursor onto the frame
|
||||||
|
func (i *ZwlrScreencopyManagerV1) CaptureOutput(overlayCursor int32, output *client.Output) (*ZwlrScreencopyFrameV1, error) {
|
||||||
|
frame := NewZwlrScreencopyFrameV1(i.Context())
|
||||||
|
const opcode = 0
|
||||||
|
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], frame.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return frame, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureOutputRegion : capture an output's region
|
||||||
|
//
|
||||||
|
// Capture the next frame of an output's region.
|
||||||
|
//
|
||||||
|
// The region is given in output logical coordinates, see
|
||||||
|
// xdg_output.logical_size. The region will be clipped to the output's
|
||||||
|
// extents.
|
||||||
|
//
|
||||||
|
// overlayCursor: composite cursor onto the frame
|
||||||
|
func (i *ZwlrScreencopyManagerV1) CaptureOutputRegion(overlayCursor int32, output *client.Output, x, y, width, height int32) (*ZwlrScreencopyFrameV1, error) {
|
||||||
|
frame := NewZwlrScreencopyFrameV1(i.Context())
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4 + 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], frame.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(x))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(y))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(width))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return frame, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the manager
|
||||||
|
//
|
||||||
|
// All objects created by the manager will still remain valid, until their
|
||||||
|
// appropriate destroy request has been called.
|
||||||
|
func (i *ZwlrScreencopyManagerV1) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 2
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1InterfaceName 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 ZwlrScreencopyFrameV1InterfaceName = "zwlr_screencopy_frame_v1"
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1 : a frame ready for copy
|
||||||
|
//
|
||||||
|
// This object represents a single frame.
|
||||||
|
//
|
||||||
|
// When created, a series of buffer events will be sent, each representing a
|
||||||
|
// supported buffer type. The "buffer_done" event is sent afterwards to
|
||||||
|
// indicate that all supported buffer types have been enumerated. The client
|
||||||
|
// will then be able to send a "copy" request. If the capture is successful,
|
||||||
|
// the compositor will send a "flags" event followed by a "ready" event.
|
||||||
|
//
|
||||||
|
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||||
|
// the "buffer" event is guaranteed to be sent.
|
||||||
|
//
|
||||||
|
// If the capture failed, the "failed" event is sent. This can happen anytime
|
||||||
|
// before the "ready" event.
|
||||||
|
//
|
||||||
|
// Once either a "ready" or a "failed" event is received, the client should
|
||||||
|
// destroy the frame.
|
||||||
|
type ZwlrScreencopyFrameV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
bufferHandler ZwlrScreencopyFrameV1BufferHandlerFunc
|
||||||
|
flagsHandler ZwlrScreencopyFrameV1FlagsHandlerFunc
|
||||||
|
readyHandler ZwlrScreencopyFrameV1ReadyHandlerFunc
|
||||||
|
failedHandler ZwlrScreencopyFrameV1FailedHandlerFunc
|
||||||
|
damageHandler ZwlrScreencopyFrameV1DamageHandlerFunc
|
||||||
|
linuxDmabufHandler ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc
|
||||||
|
bufferDoneHandler ZwlrScreencopyFrameV1BufferDoneHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrScreencopyFrameV1 : a frame ready for copy
|
||||||
|
//
|
||||||
|
// This object represents a single frame.
|
||||||
|
//
|
||||||
|
// When created, a series of buffer events will be sent, each representing a
|
||||||
|
// supported buffer type. The "buffer_done" event is sent afterwards to
|
||||||
|
// indicate that all supported buffer types have been enumerated. The client
|
||||||
|
// will then be able to send a "copy" request. If the capture is successful,
|
||||||
|
// the compositor will send a "flags" event followed by a "ready" event.
|
||||||
|
//
|
||||||
|
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||||
|
// the "buffer" event is guaranteed to be sent.
|
||||||
|
//
|
||||||
|
// If the capture failed, the "failed" event is sent. This can happen anytime
|
||||||
|
// before the "ready" event.
|
||||||
|
//
|
||||||
|
// Once either a "ready" or a "failed" event is received, the client should
|
||||||
|
// destroy the frame.
|
||||||
|
func NewZwlrScreencopyFrameV1(ctx *client.Context) *ZwlrScreencopyFrameV1 {
|
||||||
|
zwlrScreencopyFrameV1 := &ZwlrScreencopyFrameV1{}
|
||||||
|
ctx.Register(zwlrScreencopyFrameV1)
|
||||||
|
return zwlrScreencopyFrameV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy : copy the frame
|
||||||
|
//
|
||||||
|
// Copy the frame to the supplied buffer. The buffer must have the
|
||||||
|
// correct size, see zwlr_screencopy_frame_v1.buffer and
|
||||||
|
// zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
|
||||||
|
// supported format.
|
||||||
|
//
|
||||||
|
// If the frame is successfully copied, "flags" and "ready" events are
|
||||||
|
// sent. Otherwise, a "failed" event is sent.
|
||||||
|
func (i *ZwlrScreencopyFrameV1) Copy(buffer *client.Buffer) error {
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8 + 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], buffer.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : delete this object, used or not
|
||||||
|
//
|
||||||
|
// Destroys the frame. This request can be sent at any time by the client.
|
||||||
|
func (i *ZwlrScreencopyFrameV1) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 1
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyWithDamage : copy the frame when it's damaged
|
||||||
|
//
|
||||||
|
// Same as copy, except it waits until there is damage to copy.
|
||||||
|
func (i *ZwlrScreencopyFrameV1) CopyWithDamage(buffer *client.Buffer) error {
|
||||||
|
const opcode = 2
|
||||||
|
const _reqBufLen = 8 + 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], buffer.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrScreencopyFrameV1Error uint32
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1Error :
|
||||||
|
const (
|
||||||
|
// ZwlrScreencopyFrameV1ErrorAlreadyUsed : the object has already been used to copy a wl_buffer
|
||||||
|
ZwlrScreencopyFrameV1ErrorAlreadyUsed ZwlrScreencopyFrameV1Error = 0
|
||||||
|
// ZwlrScreencopyFrameV1ErrorInvalidBuffer : buffer attributes are invalid
|
||||||
|
ZwlrScreencopyFrameV1ErrorInvalidBuffer ZwlrScreencopyFrameV1Error = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
|
||||||
|
return "already_used"
|
||||||
|
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
|
||||||
|
return "invalid_buffer"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
|
||||||
|
return "0"
|
||||||
|
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
|
||||||
|
return "1"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrScreencopyFrameV1Flags uint32
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1Flags :
|
||||||
|
const (
|
||||||
|
// ZwlrScreencopyFrameV1FlagsYInvert : contents are y-inverted
|
||||||
|
ZwlrScreencopyFrameV1FlagsYInvert ZwlrScreencopyFrameV1Flags = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Flags) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1FlagsYInvert:
|
||||||
|
return "y_invert"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Flags) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1FlagsYInvert:
|
||||||
|
return "1"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Flags) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1BufferEvent : wl_shm buffer information
|
||||||
|
//
|
||||||
|
// Provides information about wl_shm buffer parameters that need to be
|
||||||
|
// used for this frame. This event is sent once after the frame is created
|
||||||
|
// if wl_shm buffers are supported.
|
||||||
|
type ZwlrScreencopyFrameV1BufferEvent struct {
|
||||||
|
Format uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
Stride uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1BufferHandlerFunc func(ZwlrScreencopyFrameV1BufferEvent)
|
||||||
|
|
||||||
|
// SetBufferHandler : sets handler for ZwlrScreencopyFrameV1BufferEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetBufferHandler(f ZwlrScreencopyFrameV1BufferHandlerFunc) {
|
||||||
|
i.bufferHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1FlagsEvent : frame flags
|
||||||
|
//
|
||||||
|
// Provides flags about the frame. This event is sent once before the
|
||||||
|
// "ready" event.
|
||||||
|
type ZwlrScreencopyFrameV1FlagsEvent struct {
|
||||||
|
Flags uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1FlagsHandlerFunc func(ZwlrScreencopyFrameV1FlagsEvent)
|
||||||
|
|
||||||
|
// SetFlagsHandler : sets handler for ZwlrScreencopyFrameV1FlagsEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetFlagsHandler(f ZwlrScreencopyFrameV1FlagsHandlerFunc) {
|
||||||
|
i.flagsHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1ReadyEvent : indicates frame is available for reading
|
||||||
|
//
|
||||||
|
// Called as soon as the frame is copied, indicating it is available
|
||||||
|
// for reading. This event includes the time at which the presentation took place.
|
||||||
|
//
|
||||||
|
// The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
|
||||||
|
// each component being an unsigned 32-bit value. Whole seconds are in
|
||||||
|
// tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
|
||||||
|
// and the additional fractional part in tv_nsec as nanoseconds. Hence,
|
||||||
|
// for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
|
||||||
|
// may have an arbitrary offset at start.
|
||||||
|
//
|
||||||
|
// After receiving this event, the client should destroy the object.
|
||||||
|
type ZwlrScreencopyFrameV1ReadyEvent struct {
|
||||||
|
TvSecHi uint32
|
||||||
|
TvSecLo uint32
|
||||||
|
TvNsec uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1ReadyHandlerFunc func(ZwlrScreencopyFrameV1ReadyEvent)
|
||||||
|
|
||||||
|
// SetReadyHandler : sets handler for ZwlrScreencopyFrameV1ReadyEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetReadyHandler(f ZwlrScreencopyFrameV1ReadyHandlerFunc) {
|
||||||
|
i.readyHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1FailedEvent : frame copy failed
|
||||||
|
//
|
||||||
|
// This event indicates that the attempted frame copy has failed.
|
||||||
|
//
|
||||||
|
// After receiving this event, the client should destroy the object.
|
||||||
|
type ZwlrScreencopyFrameV1FailedEvent struct{}
|
||||||
|
type ZwlrScreencopyFrameV1FailedHandlerFunc func(ZwlrScreencopyFrameV1FailedEvent)
|
||||||
|
|
||||||
|
// SetFailedHandler : sets handler for ZwlrScreencopyFrameV1FailedEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetFailedHandler(f ZwlrScreencopyFrameV1FailedHandlerFunc) {
|
||||||
|
i.failedHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1DamageEvent : carries the coordinates of the damaged region
|
||||||
|
//
|
||||||
|
// This event is sent right before the ready event when copy_with_damage is
|
||||||
|
// requested. It may be generated multiple times for each copy_with_damage
|
||||||
|
// request.
|
||||||
|
//
|
||||||
|
// The arguments describe a box around an area that has changed since the
|
||||||
|
// last copy request that was derived from the current screencopy manager
|
||||||
|
// instance.
|
||||||
|
//
|
||||||
|
// The union of all regions received between the call to copy_with_damage
|
||||||
|
// and a ready event is the total damage since the prior ready event.
|
||||||
|
type ZwlrScreencopyFrameV1DamageEvent struct {
|
||||||
|
X uint32
|
||||||
|
Y uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1DamageHandlerFunc func(ZwlrScreencopyFrameV1DamageEvent)
|
||||||
|
|
||||||
|
// SetDamageHandler : sets handler for ZwlrScreencopyFrameV1DamageEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetDamageHandler(f ZwlrScreencopyFrameV1DamageHandlerFunc) {
|
||||||
|
i.damageHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1LinuxDmabufEvent : linux-dmabuf buffer information
|
||||||
|
//
|
||||||
|
// Provides information about linux-dmabuf buffer parameters that need to
|
||||||
|
// be used for this frame. This event is sent once after the frame is
|
||||||
|
// created if linux-dmabuf buffers are supported.
|
||||||
|
type ZwlrScreencopyFrameV1LinuxDmabufEvent struct {
|
||||||
|
Format uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc func(ZwlrScreencopyFrameV1LinuxDmabufEvent)
|
||||||
|
|
||||||
|
// SetLinuxDmabufHandler : sets handler for ZwlrScreencopyFrameV1LinuxDmabufEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetLinuxDmabufHandler(f ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc) {
|
||||||
|
i.linuxDmabufHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1BufferDoneEvent : all buffer types reported
|
||||||
|
//
|
||||||
|
// This event is sent once after all buffer events have been sent.
|
||||||
|
//
|
||||||
|
// The client should proceed to create a buffer of one of the supported
|
||||||
|
// types, and send a "copy" request.
|
||||||
|
type ZwlrScreencopyFrameV1BufferDoneEvent struct{}
|
||||||
|
type ZwlrScreencopyFrameV1BufferDoneHandlerFunc func(ZwlrScreencopyFrameV1BufferDoneEvent)
|
||||||
|
|
||||||
|
// SetBufferDoneHandler : sets handler for ZwlrScreencopyFrameV1BufferDoneEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetBufferDoneHandler(f ZwlrScreencopyFrameV1BufferDoneHandlerFunc) {
|
||||||
|
i.bufferDoneHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ZwlrScreencopyFrameV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||||
|
switch opcode {
|
||||||
|
case 0:
|
||||||
|
if i.bufferHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1BufferEvent
|
||||||
|
l := 0
|
||||||
|
e.Format = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Stride = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.bufferHandler(e)
|
||||||
|
case 1:
|
||||||
|
if i.flagsHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1FlagsEvent
|
||||||
|
l := 0
|
||||||
|
e.Flags = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.flagsHandler(e)
|
||||||
|
case 2:
|
||||||
|
if i.readyHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1ReadyEvent
|
||||||
|
l := 0
|
||||||
|
e.TvSecHi = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.TvSecLo = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.TvNsec = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.readyHandler(e)
|
||||||
|
case 3:
|
||||||
|
if i.failedHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1FailedEvent
|
||||||
|
|
||||||
|
i.failedHandler(e)
|
||||||
|
case 4:
|
||||||
|
if i.damageHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1DamageEvent
|
||||||
|
l := 0
|
||||||
|
e.X = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Y = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.damageHandler(e)
|
||||||
|
case 5:
|
||||||
|
if i.linuxDmabufHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1LinuxDmabufEvent
|
||||||
|
l := 0
|
||||||
|
e.Format = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.linuxDmabufHandler(e)
|
||||||
|
case 6:
|
||||||
|
if i.bufferDoneHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1BufferDoneEvent
|
||||||
|
|
||||||
|
i.bufferDoneHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
399
core/internal/proto/wp_viewporter/viewporter.go
Normal file
399
core/internal/proto/wp_viewporter/viewporter.go
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : /tmp/viewporter.xml
|
||||||
|
//
|
||||||
|
// viewporter Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2013-2016 Collabora, Ltd.
|
||||||
|
//
|
||||||
|
// 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 wp_viewporter
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
|
||||||
|
// WpViewporterInterfaceName 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 WpViewporterInterfaceName = "wp_viewporter"
|
||||||
|
|
||||||
|
// WpViewporter : surface cropping and scaling
|
||||||
|
//
|
||||||
|
// The global interface exposing surface cropping and scaling
|
||||||
|
// capabilities is used to instantiate an interface extension for a
|
||||||
|
// wl_surface object. This extended interface will then allow
|
||||||
|
// cropping and scaling the surface contents, effectively
|
||||||
|
// disconnecting the direct relationship between the buffer and the
|
||||||
|
// surface size.
|
||||||
|
type WpViewporter struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWpViewporter : surface cropping and scaling
|
||||||
|
//
|
||||||
|
// The global interface exposing surface cropping and scaling
|
||||||
|
// capabilities is used to instantiate an interface extension for a
|
||||||
|
// wl_surface object. This extended interface will then allow
|
||||||
|
// cropping and scaling the surface contents, effectively
|
||||||
|
// disconnecting the direct relationship between the buffer and the
|
||||||
|
// surface size.
|
||||||
|
func NewWpViewporter(ctx *client.Context) *WpViewporter {
|
||||||
|
wpViewporter := &WpViewporter{}
|
||||||
|
ctx.Register(wpViewporter)
|
||||||
|
return wpViewporter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : unbind from the cropping and scaling interface
|
||||||
|
//
|
||||||
|
// Informs the server that the client will not be using this
|
||||||
|
// protocol object anymore. This does not affect any other objects,
|
||||||
|
// wp_viewport objects included.
|
||||||
|
func (i *WpViewporter) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 0
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewport : extend surface interface for crop and scale
|
||||||
|
//
|
||||||
|
// Instantiate an interface extension for the given wl_surface to
|
||||||
|
// crop and scale its content. If the given wl_surface already has
|
||||||
|
// a wp_viewport object associated, the viewport_exists
|
||||||
|
// protocol error is raised.
|
||||||
|
//
|
||||||
|
// surface: the surface
|
||||||
|
func (i *WpViewporter) GetViewport(surface *client.Surface) (*WpViewport, error) {
|
||||||
|
id := NewWpViewport(i.Context())
|
||||||
|
const opcode = 1
|
||||||
|
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], id.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type WpViewporterError uint32
|
||||||
|
|
||||||
|
// WpViewporterError :
|
||||||
|
const (
|
||||||
|
// WpViewporterErrorViewportExists : the surface already has a viewport object associated
|
||||||
|
WpViewporterErrorViewportExists WpViewporterError = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e WpViewporterError) Name() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewporterErrorViewportExists:
|
||||||
|
return "viewport_exists"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewporterError) Value() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewporterErrorViewportExists:
|
||||||
|
return "0"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewporterError) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WpViewportInterfaceName 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 WpViewportInterfaceName = "wp_viewport"
|
||||||
|
|
||||||
|
// WpViewport : crop and scale interface to a wl_surface
|
||||||
|
//
|
||||||
|
// An additional interface to a wl_surface object, which allows the
|
||||||
|
// client to specify the cropping and scaling of the surface
|
||||||
|
// contents.
|
||||||
|
//
|
||||||
|
// This interface works with two concepts: the source rectangle (src_x,
|
||||||
|
// src_y, src_width, src_height), and the destination size (dst_width,
|
||||||
|
// dst_height). The contents of the source rectangle are scaled to the
|
||||||
|
// destination size, and content outside the source rectangle is ignored.
|
||||||
|
// This state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// The two parts of crop and scale state are independent: the source
|
||||||
|
// rectangle, and the destination size. Initially both are unset, that
|
||||||
|
// is, no scaling is applied. The whole of the current wl_buffer is
|
||||||
|
// used as the source, and the surface size is as defined in
|
||||||
|
// wl_surface.attach.
|
||||||
|
//
|
||||||
|
// If the destination size is set, it causes the surface size to become
|
||||||
|
// dst_width, dst_height. The source (rectangle) is scaled to exactly
|
||||||
|
// this size. This overrides whatever the attached wl_buffer size is,
|
||||||
|
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
|
||||||
|
// has no content and therefore no size. Otherwise, the size is always
|
||||||
|
// at least 1x1 in surface local coordinates.
|
||||||
|
//
|
||||||
|
// If the source rectangle is set, it defines what area of the wl_buffer is
|
||||||
|
// taken as the source. If the source rectangle is set and the destination
|
||||||
|
// size is not set, then src_width and src_height must be integers, and the
|
||||||
|
// surface size becomes the source rectangle size. This results in cropping
|
||||||
|
// without scaling. If src_width or src_height are not integers and
|
||||||
|
// destination size is not set, the bad_size protocol error is raised when
|
||||||
|
// the surface state is applied.
|
||||||
|
//
|
||||||
|
// The coordinate transformations from buffer pixel coordinates up to
|
||||||
|
// the surface-local coordinates happen in the following order:
|
||||||
|
// 1. buffer_transform (wl_surface.set_buffer_transform)
|
||||||
|
// 2. buffer_scale (wl_surface.set_buffer_scale)
|
||||||
|
// 3. crop and scale (wp_viewport.set*)
|
||||||
|
// This means, that the source rectangle coordinates of crop and scale
|
||||||
|
// are given in the coordinates after the buffer transform and scale,
|
||||||
|
// i.e. in the coordinates that would be the surface-local coordinates
|
||||||
|
// if the crop and scale was not applied.
|
||||||
|
//
|
||||||
|
// If src_x or src_y are negative, the bad_value protocol error is raised.
|
||||||
|
// Otherwise, if the source rectangle is partially or completely outside of
|
||||||
|
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
|
||||||
|
// when the surface state is applied. A NULL wl_buffer does not raise the
|
||||||
|
// out_of_buffer error.
|
||||||
|
//
|
||||||
|
// If the wl_surface associated with the wp_viewport is destroyed,
|
||||||
|
// all wp_viewport requests except 'destroy' raise the protocol error
|
||||||
|
// no_surface.
|
||||||
|
//
|
||||||
|
// If the wp_viewport object is destroyed, the crop and scale
|
||||||
|
// state is removed from the wl_surface. The change will be applied
|
||||||
|
// on the next wl_surface.commit.
|
||||||
|
type WpViewport struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWpViewport : crop and scale interface to a wl_surface
|
||||||
|
//
|
||||||
|
// An additional interface to a wl_surface object, which allows the
|
||||||
|
// client to specify the cropping and scaling of the surface
|
||||||
|
// contents.
|
||||||
|
//
|
||||||
|
// This interface works with two concepts: the source rectangle (src_x,
|
||||||
|
// src_y, src_width, src_height), and the destination size (dst_width,
|
||||||
|
// dst_height). The contents of the source rectangle are scaled to the
|
||||||
|
// destination size, and content outside the source rectangle is ignored.
|
||||||
|
// This state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// The two parts of crop and scale state are independent: the source
|
||||||
|
// rectangle, and the destination size. Initially both are unset, that
|
||||||
|
// is, no scaling is applied. The whole of the current wl_buffer is
|
||||||
|
// used as the source, and the surface size is as defined in
|
||||||
|
// wl_surface.attach.
|
||||||
|
//
|
||||||
|
// If the destination size is set, it causes the surface size to become
|
||||||
|
// dst_width, dst_height. The source (rectangle) is scaled to exactly
|
||||||
|
// this size. This overrides whatever the attached wl_buffer size is,
|
||||||
|
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
|
||||||
|
// has no content and therefore no size. Otherwise, the size is always
|
||||||
|
// at least 1x1 in surface local coordinates.
|
||||||
|
//
|
||||||
|
// If the source rectangle is set, it defines what area of the wl_buffer is
|
||||||
|
// taken as the source. If the source rectangle is set and the destination
|
||||||
|
// size is not set, then src_width and src_height must be integers, and the
|
||||||
|
// surface size becomes the source rectangle size. This results in cropping
|
||||||
|
// without scaling. If src_width or src_height are not integers and
|
||||||
|
// destination size is not set, the bad_size protocol error is raised when
|
||||||
|
// the surface state is applied.
|
||||||
|
//
|
||||||
|
// The coordinate transformations from buffer pixel coordinates up to
|
||||||
|
// the surface-local coordinates happen in the following order:
|
||||||
|
// 1. buffer_transform (wl_surface.set_buffer_transform)
|
||||||
|
// 2. buffer_scale (wl_surface.set_buffer_scale)
|
||||||
|
// 3. crop and scale (wp_viewport.set*)
|
||||||
|
// This means, that the source rectangle coordinates of crop and scale
|
||||||
|
// are given in the coordinates after the buffer transform and scale,
|
||||||
|
// i.e. in the coordinates that would be the surface-local coordinates
|
||||||
|
// if the crop and scale was not applied.
|
||||||
|
//
|
||||||
|
// If src_x or src_y are negative, the bad_value protocol error is raised.
|
||||||
|
// Otherwise, if the source rectangle is partially or completely outside of
|
||||||
|
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
|
||||||
|
// when the surface state is applied. A NULL wl_buffer does not raise the
|
||||||
|
// out_of_buffer error.
|
||||||
|
//
|
||||||
|
// If the wl_surface associated with the wp_viewport is destroyed,
|
||||||
|
// all wp_viewport requests except 'destroy' raise the protocol error
|
||||||
|
// no_surface.
|
||||||
|
//
|
||||||
|
// If the wp_viewport object is destroyed, the crop and scale
|
||||||
|
// state is removed from the wl_surface. The change will be applied
|
||||||
|
// on the next wl_surface.commit.
|
||||||
|
func NewWpViewport(ctx *client.Context) *WpViewport {
|
||||||
|
wpViewport := &WpViewport{}
|
||||||
|
ctx.Register(wpViewport)
|
||||||
|
return wpViewport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : remove scaling and cropping from the surface
|
||||||
|
//
|
||||||
|
// The associated wl_surface's crop and scale state is removed.
|
||||||
|
// The change is applied on the next wl_surface.commit.
|
||||||
|
func (i *WpViewport) Destroy() error {
|
||||||
|
defer i.Context().Unregister(i)
|
||||||
|
const opcode = 0
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSource : set the source rectangle for cropping
|
||||||
|
//
|
||||||
|
// Set the source rectangle of the associated wl_surface. See
|
||||||
|
// wp_viewport for the description, and relation to the wl_buffer
|
||||||
|
// size.
|
||||||
|
//
|
||||||
|
// If all of x, y, width and height are -1.0, the source rectangle is
|
||||||
|
// unset instead. Any other set of values where width or height are zero
|
||||||
|
// or negative, or x or y are negative, raise the bad_value protocol
|
||||||
|
// error.
|
||||||
|
//
|
||||||
|
// The crop and scale state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// x: source rectangle x
|
||||||
|
// y: source rectangle y
|
||||||
|
// width: source rectangle width
|
||||||
|
// height: source rectangle height
|
||||||
|
func (i *WpViewport) SetSource(x, y, width, height float64) error {
|
||||||
|
const opcode = 1
|
||||||
|
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.PutFixed(_reqBuf[l:l+4], x)
|
||||||
|
l += 4
|
||||||
|
client.PutFixed(_reqBuf[l:l+4], y)
|
||||||
|
l += 4
|
||||||
|
client.PutFixed(_reqBuf[l:l+4], width)
|
||||||
|
l += 4
|
||||||
|
client.PutFixed(_reqBuf[l:l+4], height)
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDestination : set the surface size for scaling
|
||||||
|
//
|
||||||
|
// Set the destination size of the associated wl_surface. See
|
||||||
|
// wp_viewport for the description, and relation to the wl_buffer
|
||||||
|
// size.
|
||||||
|
//
|
||||||
|
// If width is -1 and height is -1, the destination size is unset
|
||||||
|
// instead. Any other pair of values for width and height that
|
||||||
|
// contains zero or negative values raises the bad_value protocol
|
||||||
|
// error.
|
||||||
|
//
|
||||||
|
// The crop and scale state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// width: surface width
|
||||||
|
// height: surface height
|
||||||
|
func (i *WpViewport) SetDestination(width, height int32) error {
|
||||||
|
const opcode = 2
|
||||||
|
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(width))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type WpViewportError uint32
|
||||||
|
|
||||||
|
// WpViewportError :
|
||||||
|
const (
|
||||||
|
// WpViewportErrorBadValue : negative or zero values in width or height
|
||||||
|
WpViewportErrorBadValue WpViewportError = 0
|
||||||
|
// WpViewportErrorBadSize : destination size is not integer
|
||||||
|
WpViewportErrorBadSize WpViewportError = 1
|
||||||
|
// WpViewportErrorOutOfBuffer : source rectangle extends outside of the content area
|
||||||
|
WpViewportErrorOutOfBuffer WpViewportError = 2
|
||||||
|
// WpViewportErrorNoSurface : the wl_surface was destroyed
|
||||||
|
WpViewportErrorNoSurface WpViewportError = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e WpViewportError) Name() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewportErrorBadValue:
|
||||||
|
return "bad_value"
|
||||||
|
case WpViewportErrorBadSize:
|
||||||
|
return "bad_size"
|
||||||
|
case WpViewportErrorOutOfBuffer:
|
||||||
|
return "out_of_buffer"
|
||||||
|
case WpViewportErrorNoSurface:
|
||||||
|
return "no_surface"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewportError) Value() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewportErrorBadValue:
|
||||||
|
return "0"
|
||||||
|
case WpViewportErrorBadSize:
|
||||||
|
return "1"
|
||||||
|
case WpViewportErrorOutOfBuffer:
|
||||||
|
return "2"
|
||||||
|
case WpViewportErrorNoSurface:
|
||||||
|
return "3"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewportError) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="keyboard_shortcuts_inhibit_unstable_v1">
|
||||||
|
|
||||||
|
<copyright>
|
||||||
|
Copyright © 2017 Red Hat Inc.
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<description summary="Protocol for inhibiting the compositor keyboard shortcuts">
|
||||||
|
This protocol specifies a way for a client to request the compositor
|
||||||
|
to ignore its own keyboard shortcuts for a given seat, so that all
|
||||||
|
key events from that seat get forwarded to a surface.
|
||||||
|
|
||||||
|
Warning! The protocol described in this file is experimental and
|
||||||
|
backward incompatible changes may be made. Backward compatible
|
||||||
|
changes may be added together with the corresponding interface
|
||||||
|
version bump.
|
||||||
|
Backward incompatible changes are done by bumping the version
|
||||||
|
number in the protocol and interface names and resetting the
|
||||||
|
interface version. Once the protocol is to be declared stable,
|
||||||
|
the 'z' prefix and the version number in the protocol and
|
||||||
|
interface names are removed and the interface version number is
|
||||||
|
reset.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<interface name="zwp_keyboard_shortcuts_inhibit_manager_v1" version="1">
|
||||||
|
<description summary="context object for keyboard grab_manager">
|
||||||
|
A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the keyboard shortcuts inhibitor object">
|
||||||
|
Destroy the keyboard shortcuts inhibitor manager.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="inhibit_shortcuts">
|
||||||
|
<description summary="create a new keyboard shortcuts inhibitor object">
|
||||||
|
Create a new keyboard shortcuts inhibitor object associated with
|
||||||
|
the given surface for the given seat.
|
||||||
|
|
||||||
|
If shortcuts are already inhibited for the specified seat and surface,
|
||||||
|
a protocol error "already_inhibited" is raised by the compositor.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="zwp_keyboard_shortcuts_inhibitor_v1"/>
|
||||||
|
<arg name="surface" type="object" interface="wl_surface"
|
||||||
|
summary="the surface that inhibits the keyboard shortcuts behavior"/>
|
||||||
|
<arg name="seat" type="object" interface="wl_seat"
|
||||||
|
summary="the wl_seat for which keyboard shortcuts should be disabled"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="already_inhibited"
|
||||||
|
value="0"
|
||||||
|
summary="the shortcuts are already inhibited for this surface"/>
|
||||||
|
</enum>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="zwp_keyboard_shortcuts_inhibitor_v1" version="1">
|
||||||
|
<description summary="context object for keyboard shortcuts inhibitor">
|
||||||
|
A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||||
|
its own keyboard shortcuts when the associated surface has keyboard
|
||||||
|
focus. As a result, when the surface has keyboard focus on the given
|
||||||
|
seat, it will receive all key events originating from the specified
|
||||||
|
seat, even those which would normally be caught by the compositor for
|
||||||
|
its own shortcuts.
|
||||||
|
|
||||||
|
The Wayland compositor is however under no obligation to disable
|
||||||
|
all of its shortcuts, and may keep some special key combo for its own
|
||||||
|
use, including but not limited to one allowing the user to forcibly
|
||||||
|
restore normal keyboard events routing in the case of an unwilling
|
||||||
|
client. The compositor may also use the same key combo to reactivate
|
||||||
|
an existing shortcut inhibitor that was previously deactivated on
|
||||||
|
user request.
|
||||||
|
|
||||||
|
When the compositor restores its own keyboard shortcuts, an
|
||||||
|
"inactive" event is emitted to notify the client that the keyboard
|
||||||
|
shortcuts inhibitor is not effectively active for the surface and
|
||||||
|
seat any more, and the client should not expect to receive all
|
||||||
|
keyboard events.
|
||||||
|
|
||||||
|
When the keyboard shortcuts inhibitor is inactive, the client has
|
||||||
|
no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||||
|
|
||||||
|
The user can chose to re-enable a previously deactivated keyboard
|
||||||
|
shortcuts inhibitor using any mechanism the compositor may offer,
|
||||||
|
in which case the compositor will send an "active" event to notify
|
||||||
|
the client.
|
||||||
|
|
||||||
|
If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||||
|
focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||||
|
compositor will restore its own keyboard shortcuts but no "inactive"
|
||||||
|
event is emitted in this case.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the keyboard shortcuts inhibitor object">
|
||||||
|
Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="active">
|
||||||
|
<description summary="shortcuts are inhibited">
|
||||||
|
This event indicates that the shortcut inhibitor is active.
|
||||||
|
|
||||||
|
The compositor sends this event every time compositor shortcuts
|
||||||
|
are inhibited on behalf of the surface. When active, the client
|
||||||
|
may receive input events normally reserved by the compositor
|
||||||
|
(see zwp_keyboard_shortcuts_inhibitor_v1).
|
||||||
|
|
||||||
|
This occurs typically when the initial request "inhibit_shortcuts"
|
||||||
|
first becomes active or when the user instructs the compositor to
|
||||||
|
re-enable and existing shortcuts inhibitor using any mechanism
|
||||||
|
offered by the compositor.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="inactive">
|
||||||
|
<description summary="shortcuts are restored">
|
||||||
|
This event indicates that the shortcuts inhibitor is inactive,
|
||||||
|
normal shortcuts processing is restored by the compositor.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
407
core/internal/proto/xml/wlr-layer-shell-unstable-v1.xml
Normal file
407
core/internal/proto/xml/wlr-layer-shell-unstable-v1.xml
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="wlr_layer_shell_unstable_v1">
|
||||||
|
<copyright>
|
||||||
|
Copyright © 2017 Drew DeVault
|
||||||
|
|
||||||
|
Permission to use, copy, modify, distribute, and sell this
|
||||||
|
software and its documentation for any purpose is hereby granted
|
||||||
|
without fee, provided that the above copyright notice appear in
|
||||||
|
all copies and that both that copyright notice and this permission
|
||||||
|
notice appear in supporting documentation, and that the name of
|
||||||
|
the copyright holders not be used in advertising or publicity
|
||||||
|
pertaining to distribution of the software without specific,
|
||||||
|
written prior permission. The copyright holders make no
|
||||||
|
representations about the suitability of this software for any
|
||||||
|
purpose. It is provided "as is" without express or implied
|
||||||
|
warranty.
|
||||||
|
|
||||||
|
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||||
|
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||||
|
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||||
|
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||||
|
THIS SOFTWARE.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<interface name="zwlr_layer_shell_v1" version="5">
|
||||||
|
<description summary="create surfaces that are layers of the desktop">
|
||||||
|
Clients can use this interface to assign the surface_layer role to
|
||||||
|
wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||||
|
rendered with a defined z-depth respective to each other. They may also be
|
||||||
|
anchored to the edges and corners of a screen and specify input handling
|
||||||
|
semantics. This interface should be suitable for the implementation of
|
||||||
|
many desktop shell components, and a broad number of other applications
|
||||||
|
that interact with the desktop.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="get_layer_surface">
|
||||||
|
<description summary="create a layer_surface from a surface">
|
||||||
|
Create a layer surface for an existing surface. This assigns the role of
|
||||||
|
layer_surface, or raises a protocol error if another role is already
|
||||||
|
assigned.
|
||||||
|
|
||||||
|
Creating a layer surface from a wl_surface which has a buffer attached
|
||||||
|
or committed is a client error, and any attempts by a client to attach
|
||||||
|
or manipulate a buffer prior to the first layer_surface.configure call
|
||||||
|
must also be treated as errors.
|
||||||
|
|
||||||
|
After creating a layer_surface object and setting it up, the client
|
||||||
|
must perform an initial commit without any buffer attached.
|
||||||
|
The compositor will reply with a layer_surface.configure event.
|
||||||
|
The client must acknowledge it and is then allowed to attach a buffer
|
||||||
|
to map the surface.
|
||||||
|
|
||||||
|
You may pass NULL for output to allow the compositor to decide which
|
||||||
|
output to use. Generally this will be the one that the user most
|
||||||
|
recently interacted with.
|
||||||
|
|
||||||
|
Clients can specify a namespace that defines the purpose of the layer
|
||||||
|
surface.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
|
||||||
|
<arg name="surface" type="object" interface="wl_surface"/>
|
||||||
|
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
|
||||||
|
<arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
|
||||||
|
<arg name="namespace" type="string" summary="namespace for the layer surface"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="role" value="0" summary="wl_surface has another role"/>
|
||||||
|
<entry name="invalid_layer" value="1" summary="layer value is invalid"/>
|
||||||
|
<entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="layer">
|
||||||
|
<description summary="available layers for surfaces">
|
||||||
|
These values indicate which layers a surface can be rendered in. They
|
||||||
|
are ordered by z depth, bottom-most first. Traditional shell surfaces
|
||||||
|
will typically be rendered between the bottom and top layers.
|
||||||
|
Fullscreen shell surfaces are typically rendered at the top layer.
|
||||||
|
Multiple surfaces can share a single layer, and ordering within a
|
||||||
|
single layer is undefined.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<entry name="background" value="0"/>
|
||||||
|
<entry name="bottom" value="1"/>
|
||||||
|
<entry name="top" value="2"/>
|
||||||
|
<entry name="overlay" value="3"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<!-- Version 3 additions -->
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor" since="3">
|
||||||
|
<description summary="destroy the layer_shell object">
|
||||||
|
This request indicates that the client will not use the layer_shell
|
||||||
|
object any more. Objects that have been created through this instance
|
||||||
|
are not affected.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="zwlr_layer_surface_v1" version="5">
|
||||||
|
<description summary="layer metadata interface">
|
||||||
|
An interface that may be implemented by a wl_surface, for surfaces that
|
||||||
|
are designed to be rendered as a layer of a stacked desktop-like
|
||||||
|
environment.
|
||||||
|
|
||||||
|
Layer surface state (layer, size, anchor, exclusive zone,
|
||||||
|
margin, interactivity) is double-buffered, and will be applied at the
|
||||||
|
time wl_surface.commit of the corresponding wl_surface is called.
|
||||||
|
|
||||||
|
Attaching a null buffer to a layer surface unmaps it.
|
||||||
|
|
||||||
|
Unmapping a layer_surface means that the surface cannot be shown by the
|
||||||
|
compositor until it is explicitly mapped again. The layer_surface
|
||||||
|
returns to the state it had right after layer_shell.get_layer_surface.
|
||||||
|
The client can re-map the surface by performing a commit without any
|
||||||
|
buffer attached, waiting for a configure event and handling it as usual.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="set_size">
|
||||||
|
<description summary="sets the size of the surface">
|
||||||
|
Sets the size of the surface in surface-local coordinates. The
|
||||||
|
compositor will display the surface centered with respect to its
|
||||||
|
anchors.
|
||||||
|
|
||||||
|
If you pass 0 for either value, the compositor will assign it and
|
||||||
|
inform you of the assignment in the configure event. You must set your
|
||||||
|
anchor to opposite edges in the dimensions you omit; not doing so is a
|
||||||
|
protocol error. Both values are 0 by default.
|
||||||
|
|
||||||
|
Size is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="width" type="uint"/>
|
||||||
|
<arg name="height" type="uint"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_anchor">
|
||||||
|
<description summary="configures the anchor point of the surface">
|
||||||
|
Requests that the compositor anchor the surface to the specified edges
|
||||||
|
and corners. If two orthogonal edges are specified (e.g. 'top' and
|
||||||
|
'left'), then the anchor point will be the intersection of the edges
|
||||||
|
(e.g. the top left corner of the output); otherwise the anchor point
|
||||||
|
will be centered on that edge, or in the center if none is specified.
|
||||||
|
|
||||||
|
Anchor is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="anchor" type="uint" enum="anchor"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_exclusive_zone">
|
||||||
|
<description summary="configures the exclusive geometry of this surface">
|
||||||
|
Requests that the compositor avoids occluding an area with other
|
||||||
|
surfaces. The compositor's use of this information is
|
||||||
|
implementation-dependent - do not assume that this region will not
|
||||||
|
actually be occluded.
|
||||||
|
|
||||||
|
A positive value is only meaningful if the surface is anchored to one
|
||||||
|
edge or an edge and both perpendicular edges. If the surface is not
|
||||||
|
anchored, anchored to only two perpendicular edges (a corner), anchored
|
||||||
|
to only two parallel edges or anchored to all edges, a positive value
|
||||||
|
will be treated the same as zero.
|
||||||
|
|
||||||
|
A positive zone is the distance from the edge in surface-local
|
||||||
|
coordinates to consider exclusive.
|
||||||
|
|
||||||
|
Surfaces that do not wish to have an exclusive zone may instead specify
|
||||||
|
how they should interact with surfaces that do. If set to zero, the
|
||||||
|
surface indicates that it would like to be moved to avoid occluding
|
||||||
|
surfaces with a positive exclusive zone. If set to -1, the surface
|
||||||
|
indicates that it would not like to be moved to accommodate for other
|
||||||
|
surfaces, and the compositor should extend it all the way to the edges
|
||||||
|
it is anchored to.
|
||||||
|
|
||||||
|
For example, a panel might set its exclusive zone to 10, so that
|
||||||
|
maximized shell surfaces are not shown on top of it. A notification
|
||||||
|
might set its exclusive zone to 0, so that it is moved to avoid
|
||||||
|
occluding the panel, but shell surfaces are shown underneath it. A
|
||||||
|
wallpaper or lock screen might set their exclusive zone to -1, so that
|
||||||
|
they stretch below or over the panel.
|
||||||
|
|
||||||
|
The default value is 0.
|
||||||
|
|
||||||
|
Exclusive zone is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="zone" type="int"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_margin">
|
||||||
|
<description summary="sets a margin from the anchor point">
|
||||||
|
Requests that the surface be placed some distance away from the anchor
|
||||||
|
point on the output, in surface-local coordinates. Setting this value
|
||||||
|
for edges you are not anchored to has no effect.
|
||||||
|
|
||||||
|
The exclusive zone includes the margin.
|
||||||
|
|
||||||
|
Margin is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="top" type="int"/>
|
||||||
|
<arg name="right" type="int"/>
|
||||||
|
<arg name="bottom" type="int"/>
|
||||||
|
<arg name="left" type="int"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="keyboard_interactivity">
|
||||||
|
<description summary="types of keyboard interaction possible for a layer shell surface">
|
||||||
|
Types of keyboard interaction possible for layer shell surfaces. The
|
||||||
|
rationale for this is twofold: (1) some applications are not interested
|
||||||
|
in keyboard events and not allowing them to be focused can improve the
|
||||||
|
desktop experience; (2) some applications will want to take exclusive
|
||||||
|
keyboard focus.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<entry name="none" value="0">
|
||||||
|
<description summary="no keyboard focus is possible">
|
||||||
|
This value indicates that this surface is not interested in keyboard
|
||||||
|
events and the compositor should never assign it the keyboard focus.
|
||||||
|
|
||||||
|
This is the default value, set for newly created layer shell surfaces.
|
||||||
|
|
||||||
|
This is useful for e.g. desktop widgets that display information or
|
||||||
|
only have interaction with non-keyboard input devices.
|
||||||
|
</description>
|
||||||
|
</entry>
|
||||||
|
<entry name="exclusive" value="1">
|
||||||
|
<description summary="request exclusive keyboard focus">
|
||||||
|
Request exclusive keyboard focus if this surface is above the shell surface layer.
|
||||||
|
|
||||||
|
For the top and overlay layers, the seat will always give
|
||||||
|
exclusive keyboard focus to the top-most layer which has keyboard
|
||||||
|
interactivity set to exclusive. If this layer contains multiple
|
||||||
|
surfaces with keyboard interactivity set to exclusive, the compositor
|
||||||
|
determines the one receiving keyboard events in an implementation-
|
||||||
|
defined manner. In this case, no guarantee is made when this surface
|
||||||
|
will receive keyboard focus (if ever).
|
||||||
|
|
||||||
|
For the bottom and background layers, the compositor is allowed to use
|
||||||
|
normal focus semantics.
|
||||||
|
|
||||||
|
This setting is mainly intended for applications that need to ensure
|
||||||
|
they receive all keyboard events, such as a lock screen or a password
|
||||||
|
prompt.
|
||||||
|
</description>
|
||||||
|
</entry>
|
||||||
|
<entry name="on_demand" value="2" since="4">
|
||||||
|
<description summary="request regular keyboard focus semantics">
|
||||||
|
This requests the compositor to allow this surface to be focused and
|
||||||
|
unfocused by the user in an implementation-defined manner. The user
|
||||||
|
should be able to unfocus this surface even regardless of the layer
|
||||||
|
it is on.
|
||||||
|
|
||||||
|
Typically, the compositor will want to use its normal mechanism to
|
||||||
|
manage keyboard focus between layer shell surfaces with this setting
|
||||||
|
and regular toplevels on the desktop layer (e.g. click to focus).
|
||||||
|
Nevertheless, it is possible for a compositor to require a special
|
||||||
|
interaction to focus or unfocus layer shell surfaces (e.g. requiring
|
||||||
|
a click even if focus follows the mouse normally, or providing a
|
||||||
|
keybinding to switch focus between layers).
|
||||||
|
|
||||||
|
This setting is mainly intended for desktop shell components (e.g.
|
||||||
|
panels) that allow keyboard interaction. Using this option can allow
|
||||||
|
implementing a desktop shell that can be fully usable without the
|
||||||
|
mouse.
|
||||||
|
</description>
|
||||||
|
</entry>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="set_keyboard_interactivity">
|
||||||
|
<description summary="requests keyboard events">
|
||||||
|
Set how keyboard events are delivered to this surface. By default,
|
||||||
|
layer shell surfaces do not receive keyboard events; this request can
|
||||||
|
be used to change this.
|
||||||
|
|
||||||
|
This setting is inherited by child surfaces set by the get_popup
|
||||||
|
request.
|
||||||
|
|
||||||
|
Layer surfaces receive pointer, touch, and tablet events normally. If
|
||||||
|
you do not want to receive them, set the input region on your surface
|
||||||
|
to an empty region.
|
||||||
|
|
||||||
|
Keyboard interactivity is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="get_popup">
|
||||||
|
<description summary="assign this layer_surface as an xdg_popup parent">
|
||||||
|
This assigns an xdg_popup's parent to this layer_surface. This popup
|
||||||
|
should have been created via xdg_surface::get_popup with the parent set
|
||||||
|
to NULL, and this request must be invoked before committing the popup's
|
||||||
|
initial state.
|
||||||
|
|
||||||
|
See the documentation of xdg_popup for more details about what an
|
||||||
|
xdg_popup is and how it is used.
|
||||||
|
</description>
|
||||||
|
<arg name="popup" type="object" interface="xdg_popup"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="ack_configure">
|
||||||
|
<description summary="ack a configure event">
|
||||||
|
When a configure event is received, if a client commits the
|
||||||
|
surface in response to the configure event, then the client
|
||||||
|
must make an ack_configure request sometime before the commit
|
||||||
|
request, passing along the serial of the configure event.
|
||||||
|
|
||||||
|
If the client receives multiple configure events before it
|
||||||
|
can respond to one, it only has to ack the last configure event.
|
||||||
|
|
||||||
|
A client is not required to commit immediately after sending
|
||||||
|
an ack_configure request - it may even ack_configure several times
|
||||||
|
before its next surface commit.
|
||||||
|
|
||||||
|
A client may send multiple ack_configure requests before committing, but
|
||||||
|
only the last request sent before a commit indicates which configure
|
||||||
|
event the client really is responding to.
|
||||||
|
</description>
|
||||||
|
<arg name="serial" type="uint" summary="the serial from the configure event"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the layer_surface">
|
||||||
|
This request destroys the layer surface.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="configure">
|
||||||
|
<description summary="suggest a surface change">
|
||||||
|
The configure event asks the client to resize its surface.
|
||||||
|
|
||||||
|
Clients should arrange their surface for the new states, and then send
|
||||||
|
an ack_configure request with the serial sent in this configure event at
|
||||||
|
some point before committing the new surface.
|
||||||
|
|
||||||
|
The client is free to dismiss all but the last configure event it
|
||||||
|
received.
|
||||||
|
|
||||||
|
The width and height arguments specify the size of the window in
|
||||||
|
surface-local coordinates.
|
||||||
|
|
||||||
|
The size is a hint, in the sense that the client is free to ignore it if
|
||||||
|
it doesn't resize, pick a smaller size (to satisfy aspect ratio or
|
||||||
|
resize in steps of NxM pixels). If the client picks a smaller size and
|
||||||
|
is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
|
||||||
|
surface will be centered on this axis.
|
||||||
|
|
||||||
|
If the width or height arguments are zero, it means the client should
|
||||||
|
decide its own window dimension.
|
||||||
|
</description>
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
<arg name="width" type="uint"/>
|
||||||
|
<arg name="height" type="uint"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="closed">
|
||||||
|
<description summary="surface should be closed">
|
||||||
|
The closed event is sent by the compositor when the surface will no
|
||||||
|
longer be shown. The output may have been destroyed or the user may
|
||||||
|
have asked for it to be removed. Further changes to the surface will be
|
||||||
|
ignored. The client should destroy the resource after receiving this
|
||||||
|
event, and create a new surface if they so choose.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
|
||||||
|
<entry name="invalid_size" value="1" summary="size is invalid"/>
|
||||||
|
<entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
|
||||||
|
<entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
|
||||||
|
<entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="anchor" bitfield="true">
|
||||||
|
<entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
|
||||||
|
<entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
|
||||||
|
<entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
|
||||||
|
<entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<!-- Version 2 additions -->
|
||||||
|
|
||||||
|
<request name="set_layer" since="2">
|
||||||
|
<description summary="change the layer of the surface">
|
||||||
|
Change the layer that the surface is rendered on.
|
||||||
|
|
||||||
|
Layer is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<!-- Version 5 additions -->
|
||||||
|
|
||||||
|
<request name="set_exclusive_edge" since="5">
|
||||||
|
<description summary="set the edge the exclusive zone will be applied to">
|
||||||
|
Requests an edge for the exclusive zone to apply. The exclusive
|
||||||
|
edge will be automatically deduced from anchor points when possible,
|
||||||
|
but when the surface is anchored to a corner, it will be necessary
|
||||||
|
to set it explicitly to disambiguate, as it is not possible to deduce
|
||||||
|
which one of the two corner edges should be used.
|
||||||
|
|
||||||
|
The edge must be one the surface is anchored to, otherwise the
|
||||||
|
invalid_exclusive_edge protocol error will be raised.
|
||||||
|
</description>
|
||||||
|
<arg name="edge" type="uint" enum="anchor"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
234
core/internal/proto/xml/wlr-screencopy-unstable-v1.xml
Normal file
234
core/internal/proto/xml/wlr-screencopy-unstable-v1.xml
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="wlr_screencopy_unstable_v1">
|
||||||
|
<copyright>
|
||||||
|
Copyright © 2018 Simon Ser
|
||||||
|
Copyright © 2019 Andri Yngvason
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<description summary="screen content capturing on client buffers">
|
||||||
|
This protocol allows clients to ask the compositor to copy part of the
|
||||||
|
screen content to a client buffer.
|
||||||
|
|
||||||
|
Warning! The protocol described in this file is experimental and
|
||||||
|
backward incompatible changes may be made. Backward compatible changes
|
||||||
|
may be added together with the corresponding interface version bump.
|
||||||
|
Backward incompatible changes are done by bumping the version number in
|
||||||
|
the protocol and interface names and resetting the interface version.
|
||||||
|
Once the protocol is to be declared stable, the 'z' prefix and the
|
||||||
|
version number in the protocol and interface names are removed and the
|
||||||
|
interface version number is reset.
|
||||||
|
|
||||||
|
Note! This protocol is deprecated and not intended for production use.
|
||||||
|
The ext-image-copy-capture-v1 protocol should be used instead.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<interface name="zwlr_screencopy_manager_v1" version="3">
|
||||||
|
<description summary="manager to inform clients and begin capturing">
|
||||||
|
This object is a manager which offers requests to start capturing from a
|
||||||
|
source.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="capture_output">
|
||||||
|
<description summary="capture an output">
|
||||||
|
Capture the next frame of an entire output.
|
||||||
|
</description>
|
||||||
|
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||||
|
<arg name="overlay_cursor" type="int"
|
||||||
|
summary="composite cursor onto the frame"/>
|
||||||
|
<arg name="output" type="object" interface="wl_output"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="capture_output_region">
|
||||||
|
<description summary="capture an output's region">
|
||||||
|
Capture the next frame of an output's region.
|
||||||
|
|
||||||
|
The region is given in output logical coordinates, see
|
||||||
|
xdg_output.logical_size. The region will be clipped to the output's
|
||||||
|
extents.
|
||||||
|
</description>
|
||||||
|
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||||
|
<arg name="overlay_cursor" type="int"
|
||||||
|
summary="composite cursor onto the frame"/>
|
||||||
|
<arg name="output" type="object" interface="wl_output"/>
|
||||||
|
<arg name="x" type="int"/>
|
||||||
|
<arg name="y" type="int"/>
|
||||||
|
<arg name="width" type="int"/>
|
||||||
|
<arg name="height" type="int"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the manager">
|
||||||
|
All objects created by the manager will still remain valid, until their
|
||||||
|
appropriate destroy request has been called.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="zwlr_screencopy_frame_v1" version="3">
|
||||||
|
<description summary="a frame ready for copy">
|
||||||
|
This object represents a single frame.
|
||||||
|
|
||||||
|
When created, a series of buffer events will be sent, each representing a
|
||||||
|
supported buffer type. The "buffer_done" event is sent afterwards to
|
||||||
|
indicate that all supported buffer types have been enumerated. The client
|
||||||
|
will then be able to send a "copy" request. If the capture is successful,
|
||||||
|
the compositor will send a "flags" event followed by a "ready" event.
|
||||||
|
|
||||||
|
For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||||
|
the "buffer" event is guaranteed to be sent.
|
||||||
|
|
||||||
|
If the capture failed, the "failed" event is sent. This can happen anytime
|
||||||
|
before the "ready" event.
|
||||||
|
|
||||||
|
Once either a "ready" or a "failed" event is received, the client should
|
||||||
|
destroy the frame.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<event name="buffer">
|
||||||
|
<description summary="wl_shm buffer information">
|
||||||
|
Provides information about wl_shm buffer parameters that need to be
|
||||||
|
used for this frame. This event is sent once after the frame is created
|
||||||
|
if wl_shm buffers are supported.
|
||||||
|
</description>
|
||||||
|
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
|
||||||
|
<arg name="width" type="uint" summary="buffer width"/>
|
||||||
|
<arg name="height" type="uint" summary="buffer height"/>
|
||||||
|
<arg name="stride" type="uint" summary="buffer stride"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<request name="copy">
|
||||||
|
<description summary="copy the frame">
|
||||||
|
Copy the frame to the supplied buffer. The buffer must have the
|
||||||
|
correct size, see zwlr_screencopy_frame_v1.buffer and
|
||||||
|
zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
|
||||||
|
supported format.
|
||||||
|
|
||||||
|
If the frame is successfully copied, "flags" and "ready" events are
|
||||||
|
sent. Otherwise, a "failed" event is sent.
|
||||||
|
</description>
|
||||||
|
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="already_used" value="0"
|
||||||
|
summary="the object has already been used to copy a wl_buffer"/>
|
||||||
|
<entry name="invalid_buffer" value="1"
|
||||||
|
summary="buffer attributes are invalid"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="flags" bitfield="true">
|
||||||
|
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<event name="flags">
|
||||||
|
<description summary="frame flags">
|
||||||
|
Provides flags about the frame. This event is sent once before the
|
||||||
|
"ready" event.
|
||||||
|
</description>
|
||||||
|
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="ready">
|
||||||
|
<description summary="indicates frame is available for reading">
|
||||||
|
Called as soon as the frame is copied, indicating it is available
|
||||||
|
for reading. This event includes the time at which the presentation took place.
|
||||||
|
|
||||||
|
The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
|
||||||
|
each component being an unsigned 32-bit value. Whole seconds are in
|
||||||
|
tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
|
||||||
|
and the additional fractional part in tv_nsec as nanoseconds. Hence,
|
||||||
|
for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
|
||||||
|
may have an arbitrary offset at start.
|
||||||
|
|
||||||
|
After receiving this event, the client should destroy the object.
|
||||||
|
</description>
|
||||||
|
<arg name="tv_sec_hi" type="uint"
|
||||||
|
summary="high 32 bits of the seconds part of the timestamp"/>
|
||||||
|
<arg name="tv_sec_lo" type="uint"
|
||||||
|
summary="low 32 bits of the seconds part of the timestamp"/>
|
||||||
|
<arg name="tv_nsec" type="uint"
|
||||||
|
summary="nanoseconds part of the timestamp"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="failed">
|
||||||
|
<description summary="frame copy failed">
|
||||||
|
This event indicates that the attempted frame copy has failed.
|
||||||
|
|
||||||
|
After receiving this event, the client should destroy the object.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="delete this object, used or not">
|
||||||
|
Destroys the frame. This request can be sent at any time by the client.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<!-- Version 2 additions -->
|
||||||
|
<request name="copy_with_damage" since="2">
|
||||||
|
<description summary="copy the frame when it's damaged">
|
||||||
|
Same as copy, except it waits until there is damage to copy.
|
||||||
|
</description>
|
||||||
|
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="damage" since="2">
|
||||||
|
<description summary="carries the coordinates of the damaged region">
|
||||||
|
This event is sent right before the ready event when copy_with_damage is
|
||||||
|
requested. It may be generated multiple times for each copy_with_damage
|
||||||
|
request.
|
||||||
|
|
||||||
|
The arguments describe a box around an area that has changed since the
|
||||||
|
last copy request that was derived from the current screencopy manager
|
||||||
|
instance.
|
||||||
|
|
||||||
|
The union of all regions received between the call to copy_with_damage
|
||||||
|
and a ready event is the total damage since the prior ready event.
|
||||||
|
</description>
|
||||||
|
<arg name="x" type="uint" summary="damaged x coordinates"/>
|
||||||
|
<arg name="y" type="uint" summary="damaged y coordinates"/>
|
||||||
|
<arg name="width" type="uint" summary="current width"/>
|
||||||
|
<arg name="height" type="uint" summary="current height"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<!-- Version 3 additions -->
|
||||||
|
<event name="linux_dmabuf" since="3">
|
||||||
|
<description summary="linux-dmabuf buffer information">
|
||||||
|
Provides information about linux-dmabuf buffer parameters that need to
|
||||||
|
be used for this frame. This event is sent once after the frame is
|
||||||
|
created if linux-dmabuf buffers are supported.
|
||||||
|
</description>
|
||||||
|
<arg name="format" type="uint" summary="fourcc pixel format"/>
|
||||||
|
<arg name="width" type="uint" summary="buffer width"/>
|
||||||
|
<arg name="height" type="uint" summary="buffer height"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="buffer_done" since="3">
|
||||||
|
<description summary="all buffer types reported">
|
||||||
|
This event is sent once after all buffer events have been sent.
|
||||||
|
|
||||||
|
The client should proceed to create a buffer of one of the supported
|
||||||
|
types, and send a "copy" request.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
69
core/internal/screenshot/compositor.go
Normal file
69
core/internal/screenshot/compositor.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Compositor int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompositorUnknown Compositor = iota
|
||||||
|
CompositorHyprland
|
||||||
|
)
|
||||||
|
|
||||||
|
func DetectCompositor() Compositor {
|
||||||
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||||
|
return CompositorHyprland
|
||||||
|
}
|
||||||
|
return CompositorUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowGeometry struct {
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
Width int32
|
||||||
|
Height int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetActiveWindow() (*WindowGeometry, error) {
|
||||||
|
compositor := DetectCompositor()
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case CompositorHyprland:
|
||||||
|
return getHyprlandActiveWindow()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("window capture requires Hyprland (other compositors not yet supported)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hyprlandWindow struct {
|
||||||
|
At [2]int32 `json:"at"`
|
||||||
|
Size [2]int32 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHyprlandActiveWindow() (*WindowGeometry, error) {
|
||||||
|
cmd := exec.Command("hyprctl", "-j", "activewindow")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hyprctl activewindow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var win hyprlandWindow
|
||||||
|
if err := json.Unmarshal(output, &win); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse activewindow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if win.Size[0] <= 0 || win.Size[1] <= 0 {
|
||||||
|
return nil, fmt.Errorf("no active window")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WindowGeometry{
|
||||||
|
X: win.At[0],
|
||||||
|
Y: win.At[1],
|
||||||
|
Width: win.Size[0],
|
||||||
|
Height: win.Size[1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
197
core/internal/screenshot/encode.go
Normal file
197
core/internal/screenshot/encode.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BufferToImage(buf *ShmBuffer) *image.RGBA {
|
||||||
|
return BufferToImageWithFormat(buf, uint32(FormatARGB8888))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
|
||||||
|
data := buf.Data()
|
||||||
|
|
||||||
|
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0
|
||||||
|
|
||||||
|
for y := 0; y < buf.Height; y++ {
|
||||||
|
srcOff := y * buf.Stride
|
||||||
|
dstOff := y * img.Stride
|
||||||
|
for x := 0; x < buf.Width; x++ {
|
||||||
|
si := srcOff + x*4
|
||||||
|
di := dstOff + x*4
|
||||||
|
if si+3 >= len(data) || di+3 >= len(img.Pix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if swapRB {
|
||||||
|
img.Pix[di+0] = data[si+2]
|
||||||
|
img.Pix[di+1] = data[si+1]
|
||||||
|
img.Pix[di+2] = data[si+0]
|
||||||
|
} else {
|
||||||
|
img.Pix[di+0] = data[si+0]
|
||||||
|
img.Pix[di+1] = data[si+1]
|
||||||
|
img.Pix[di+2] = data[si+2]
|
||||||
|
}
|
||||||
|
img.Pix[di+3] = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodePNG(w io.Writer, img image.Image) error {
|
||||||
|
enc := png.Encoder{CompressionLevel: png.BestSpeed}
|
||||||
|
return enc.Encode(w, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeJPEG(w io.Writer, img image.Image, quality int) error {
|
||||||
|
return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodePPM(w io.Writer, img *image.RGBA) error {
|
||||||
|
bw := bufio.NewWriter(w)
|
||||||
|
bounds := img.Bounds()
|
||||||
|
if _, err := fmt.Fprintf(bw, "P6\n%d %d\n255\n", bounds.Dx(), bounds.Dy()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
|
off := (y-bounds.Min.Y)*img.Stride + (x-bounds.Min.X)*4
|
||||||
|
if err := bw.WriteByte(img.Pix[off+0]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := bw.WriteByte(img.Pix[off+1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := bw.WriteByte(img.Pix[off+2]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bw.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateFilename(format Format) string {
|
||||||
|
t := time.Now()
|
||||||
|
ext := "png"
|
||||||
|
switch format {
|
||||||
|
case FormatJPEG:
|
||||||
|
ext = "jpg"
|
||||||
|
case FormatPPM:
|
||||||
|
ext = "ppm"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("screenshot_%s.%s", t.Format("2006-01-02_15-04-05"), ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOutputDir() string {
|
||||||
|
if dir := os.Getenv("DMS_SCREENSHOT_DIR"); dir != "" {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
|
||||||
|
screenshotDir := filepath.Join(xdgPics, "Screenshots")
|
||||||
|
if err := os.MkdirAll(screenshotDir, 0755); err == nil {
|
||||||
|
return screenshotDir
|
||||||
|
}
|
||||||
|
return xdgPics
|
||||||
|
}
|
||||||
|
|
||||||
|
if home := os.Getenv("HOME"); home != "" {
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
func getXDGPicturesDir() string {
|
||||||
|
configDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
if configDir == "" {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
configDir = filepath.Join(home, ".config")
|
||||||
|
}
|
||||||
|
|
||||||
|
userDirsFile := filepath.Join(configDir, "user-dirs.dirs")
|
||||||
|
data, err := os.ReadFile(userDirsFile)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range splitLines(string(data)) {
|
||||||
|
if len(line) == 0 || line[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prefix = "XDG_PICTURES_DIR="
|
||||||
|
if len(line) > len(prefix) && line[:len(prefix)] == prefix {
|
||||||
|
path := line[len(prefix):]
|
||||||
|
path = trimQuotes(path)
|
||||||
|
path = expandHome(path)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitLines(s string) []string {
|
||||||
|
var lines []string
|
||||||
|
start := 0
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] == '\n' {
|
||||||
|
lines = append(lines, s[start:i])
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < len(s) {
|
||||||
|
lines = append(lines, s[start:])
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimQuotes(s string) string {
|
||||||
|
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandHome(path string) string {
|
||||||
|
if len(path) >= 5 && path[:5] == "$HOME" {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return home + path[5:]
|
||||||
|
}
|
||||||
|
if len(path) >= 1 && path[0] == '~' {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return home + path[1:]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error {
|
||||||
|
return WriteToFileWithFormat(buf, path, format, quality, uint32(FormatARGB8888))
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteToFileWithFormat(buf *ShmBuffer, path string, format Format, quality int, pixelFormat uint32) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
img := BufferToImageWithFormat(buf, pixelFormat)
|
||||||
|
switch format {
|
||||||
|
case FormatJPEG:
|
||||||
|
return EncodeJPEG(f, img, quality)
|
||||||
|
case FormatPPM:
|
||||||
|
return EncodePPM(f, img)
|
||||||
|
default:
|
||||||
|
return EncodePNG(f, img)
|
||||||
|
}
|
||||||
|
}
|
||||||
180
core/internal/screenshot/notify.go
Normal file
180
core/internal/screenshot/notify.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
notifyDest = "org.freedesktop.Notifications"
|
||||||
|
notifyPath = "/org/freedesktop/Notifications"
|
||||||
|
notifyInterface = "org.freedesktop.Notifications"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotifyResult struct {
|
||||||
|
FilePath string
|
||||||
|
Clipboard bool
|
||||||
|
ImageData []byte
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendNotification(result NotifyResult) {
|
||||||
|
conn, err := dbus.SessionBus()
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("dbus session failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions []string
|
||||||
|
if result.FilePath != "" {
|
||||||
|
actions = []string{"default", "Open"}
|
||||||
|
}
|
||||||
|
|
||||||
|
hints := map[string]dbus.Variant{}
|
||||||
|
if len(result.ImageData) > 0 && result.Width > 0 && result.Height > 0 {
|
||||||
|
rowstride := result.Width * 3
|
||||||
|
hints["image_data"] = dbus.MakeVariant(struct {
|
||||||
|
Width int32
|
||||||
|
Height int32
|
||||||
|
Rowstride int32
|
||||||
|
HasAlpha bool
|
||||||
|
BitsPerSample int32
|
||||||
|
Channels int32
|
||||||
|
Data []byte
|
||||||
|
}{
|
||||||
|
Width: int32(result.Width),
|
||||||
|
Height: int32(result.Height),
|
||||||
|
Rowstride: int32(rowstride),
|
||||||
|
HasAlpha: false,
|
||||||
|
BitsPerSample: 8,
|
||||||
|
Channels: 3,
|
||||||
|
Data: result.ImageData,
|
||||||
|
})
|
||||||
|
} else if result.FilePath != "" {
|
||||||
|
hints["image_path"] = dbus.MakeVariant(result.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := "Screenshot captured"
|
||||||
|
body := ""
|
||||||
|
if result.Clipboard && result.FilePath != "" {
|
||||||
|
body = fmt.Sprintf("Copied to clipboard\n%s", filepath.Base(result.FilePath))
|
||||||
|
} else if result.Clipboard {
|
||||||
|
body = "Copied to clipboard"
|
||||||
|
} else if result.FilePath != "" {
|
||||||
|
body = filepath.Base(result.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := conn.Object(notifyDest, notifyPath)
|
||||||
|
call := obj.Call(
|
||||||
|
notifyInterface+".Notify",
|
||||||
|
0,
|
||||||
|
"DMS",
|
||||||
|
uint32(0),
|
||||||
|
"",
|
||||||
|
summary,
|
||||||
|
body,
|
||||||
|
actions,
|
||||||
|
hints,
|
||||||
|
int32(5000),
|
||||||
|
)
|
||||||
|
|
||||||
|
if call.Err != nil {
|
||||||
|
log.Debug("notify call failed", "err", call.Err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationID uint32
|
||||||
|
if err := call.Store(¬ificationID); err != nil {
|
||||||
|
log.Debug("failed to get notification id", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(actions) == 0 || result.FilePath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnActionListener(notificationID, result.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func spawnActionListener(notificationID uint32, filePath string) {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("failed to get executable", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(exe, "notify-action", fmt.Sprintf("%d", notificationID), filePath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: true,
|
||||||
|
}
|
||||||
|
cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunNotifyActionListener(args []string) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationID, err := strconv.ParseUint(args[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := args[1]
|
||||||
|
|
||||||
|
conn, err := dbus.SessionBus()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.AddMatchSignal(
|
||||||
|
dbus.WithMatchObjectPath(notifyPath),
|
||||||
|
dbus.WithMatchInterface(notifyInterface),
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signals := make(chan *dbus.Signal, 10)
|
||||||
|
conn.Signal(signals)
|
||||||
|
|
||||||
|
for sig := range signals {
|
||||||
|
switch sig.Name {
|
||||||
|
case notifyInterface + ".ActionInvoked":
|
||||||
|
if len(sig.Body) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, ok := sig.Body[0].(uint32)
|
||||||
|
if !ok || id != uint32(notificationID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
openFile(filePath)
|
||||||
|
return
|
||||||
|
|
||||||
|
case notifyInterface + ".NotificationClosed":
|
||||||
|
if len(sig.Body) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, ok := sig.Body[0].(uint32)
|
||||||
|
if !ok || id != uint32(notificationID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFile(filePath string) {
|
||||||
|
cmd := exec.Command("xdg-open", filePath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: true,
|
||||||
|
}
|
||||||
|
cmd.Start()
|
||||||
|
}
|
||||||
805
core/internal/screenshot/region.go
Normal file
805
core/internal/screenshot/region.go
Normal file
@@ -0,0 +1,805 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectionState struct {
|
||||||
|
hasSelection bool // There's a selection to display (pre-loaded or user-drawn)
|
||||||
|
dragging bool // User is actively drawing a new selection
|
||||||
|
surface *OutputSurface // Surface where selection was made
|
||||||
|
// Surface-local logical coordinates (from pointer events)
|
||||||
|
anchorX float64
|
||||||
|
anchorY float64
|
||||||
|
currentX float64
|
||||||
|
currentY float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderSlot struct {
|
||||||
|
shm *ShmBuffer
|
||||||
|
pool *client.ShmPool
|
||||||
|
wlBuf *client.Buffer
|
||||||
|
busy bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputSurface struct {
|
||||||
|
output *WaylandOutput
|
||||||
|
wlSurface *client.Surface
|
||||||
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
|
viewport *wp_viewporter.WpViewport
|
||||||
|
screenBuf *ShmBuffer
|
||||||
|
screenBufNoCursor *ShmBuffer
|
||||||
|
screenFormat uint32
|
||||||
|
logicalW int
|
||||||
|
logicalH int
|
||||||
|
configured bool
|
||||||
|
yInverted bool
|
||||||
|
|
||||||
|
// Triple-buffered render slots
|
||||||
|
slots [3]*RenderSlot
|
||||||
|
slotsReady bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreCapture struct {
|
||||||
|
screenBuf *ShmBuffer
|
||||||
|
screenBufNoCursor *ShmBuffer
|
||||||
|
format uint32
|
||||||
|
yInverted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionSelector struct {
|
||||||
|
screenshoter *Screenshoter
|
||||||
|
|
||||||
|
display *client.Display
|
||||||
|
registry *client.Registry
|
||||||
|
ctx *client.Context
|
||||||
|
|
||||||
|
compositor *client.Compositor
|
||||||
|
shm *client.Shm
|
||||||
|
seat *client.Seat
|
||||||
|
pointer *client.Pointer
|
||||||
|
keyboard *client.Keyboard
|
||||||
|
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||||
|
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||||
|
viewporter *wp_viewporter.WpViewporter
|
||||||
|
|
||||||
|
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
||||||
|
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
||||||
|
|
||||||
|
outputs map[uint32]*WaylandOutput
|
||||||
|
outputsMu sync.Mutex
|
||||||
|
preCapture map[*WaylandOutput]*PreCapture
|
||||||
|
|
||||||
|
surfaces []*OutputSurface
|
||||||
|
activeSurface *OutputSurface
|
||||||
|
|
||||||
|
// Cursor surface for crosshair
|
||||||
|
cursorSurface *client.Surface
|
||||||
|
cursorBuffer *ShmBuffer
|
||||||
|
cursorWlBuf *client.Buffer
|
||||||
|
cursorPool *client.ShmPool
|
||||||
|
|
||||||
|
selection SelectionState
|
||||||
|
pointerX float64
|
||||||
|
pointerY float64
|
||||||
|
preSelect Region
|
||||||
|
showCapturedCursor bool
|
||||||
|
shiftHeld bool
|
||||||
|
|
||||||
|
running bool
|
||||||
|
cancelled bool
|
||||||
|
result Region
|
||||||
|
|
||||||
|
capturedBuffer *ShmBuffer
|
||||||
|
capturedRegion Region
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
||||||
|
return &RegionSelector{
|
||||||
|
screenshoter: s,
|
||||||
|
outputs: make(map[uint32]*WaylandOutput),
|
||||||
|
preCapture: make(map[*WaylandOutput]*PreCapture),
|
||||||
|
showCapturedCursor: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
||||||
|
r.preSelect = GetLastRegion()
|
||||||
|
|
||||||
|
if err := r.connect(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer r.cleanup()
|
||||||
|
|
||||||
|
if err := r.setupRegistry(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("registry setup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.roundtrip(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("roundtrip after registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.screencopy == nil:
|
||||||
|
return nil, false, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||||
|
case r.layerShell == nil:
|
||||||
|
return nil, false, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
|
||||||
|
case r.seat == nil:
|
||||||
|
return nil, false, fmt.Errorf("no seat available")
|
||||||
|
case r.compositor == nil:
|
||||||
|
return nil, false, fmt.Errorf("compositor not available")
|
||||||
|
case r.shm == nil:
|
||||||
|
return nil, false, fmt.Errorf("wl_shm not available")
|
||||||
|
case len(r.outputs) == 0:
|
||||||
|
return nil, false, fmt.Errorf("no outputs available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.roundtrip(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("roundtrip after protocol check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.preCaptureAllOutputs(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("pre-capture: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.createSurfaces(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("create surfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.createCursor()
|
||||||
|
|
||||||
|
if err := r.roundtrip(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("roundtrip after surfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.running = true
|
||||||
|
for r.running {
|
||||||
|
if err := r.ctx.Dispatch(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("dispatch: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.cancelled || r.capturedBuffer == nil {
|
||||||
|
return nil, r.cancelled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
yInverted := false
|
||||||
|
var format uint32
|
||||||
|
if r.selection.surface != nil {
|
||||||
|
yInverted = r.selection.surface.yInverted
|
||||||
|
format = r.selection.surface.screenFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: r.capturedBuffer,
|
||||||
|
Region: r.result,
|
||||||
|
YInverted: yInverted,
|
||||||
|
Format: format,
|
||||||
|
}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) connect() error {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.display = display
|
||||||
|
r.ctx = display.Context()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) roundtrip() error {
|
||||||
|
return wlhelpers.Roundtrip(r.display, r.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupRegistry() error {
|
||||||
|
registry, err := r.display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.registry = registry
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
r.handleGlobal(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
delete(r.outputs, e.Name)
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) handleGlobal(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case client.CompositorInterfaceName:
|
||||||
|
comp := client.NewCompositor(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
|
||||||
|
r.compositor = comp
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.ShmInterfaceName:
|
||||||
|
shm := client.NewShm(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||||
|
r.shm = shm
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.SeatInterfaceName:
|
||||||
|
seat := client.NewSeat(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
|
||||||
|
r.seat = seat
|
||||||
|
r.setupInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
output := client.NewOutput(r.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
r.outputs[e.Name] = &WaylandOutput{
|
||||||
|
wlOutput: output,
|
||||||
|
globalName: e.Name,
|
||||||
|
scale: 1,
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
r.setupOutputHandlers(e.Name, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||||
|
ls := wlr_layer_shell.NewZwlrLayerShellV1(r.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, version, ls); err == nil {
|
||||||
|
r.layerShell = ls
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
|
sc := wlr_screencopy.NewZwlrScreencopyManagerV1(r.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 3 {
|
||||||
|
version = 3
|
||||||
|
}
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
|
||||||
|
r.screencopy = sc
|
||||||
|
}
|
||||||
|
|
||||||
|
case wp_viewporter.WpViewporterInterfaceName:
|
||||||
|
vp := wp_viewporter.NewWpViewporter(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, vp); err == nil {
|
||||||
|
r.viewporter = vp
|
||||||
|
}
|
||||||
|
|
||||||
|
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
||||||
|
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
r.shortcutsInhibitMgr = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupOutputHandlers(name uint32, output *client.Output) {
|
||||||
|
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.x = e.X
|
||||||
|
o.y = e.Y
|
||||||
|
o.transform = int32(e.Transform)
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||||
|
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.width = e.Width
|
||||||
|
o.height = e.Height
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.scale = e.Factor
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.name = e.Name
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) preCaptureAllOutputs() error {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
outputs := make([]*WaylandOutput, 0, len(r.outputs))
|
||||||
|
for _, o := range r.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
|
||||||
|
pending := len(outputs) * 2
|
||||||
|
done := make(chan struct{}, pending)
|
||||||
|
|
||||||
|
for _, output := range outputs {
|
||||||
|
pc := &PreCapture{}
|
||||||
|
r.preCapture[output] = pc
|
||||||
|
|
||||||
|
r.preCaptureOutput(output, pc, true, func() { done <- struct{}{} })
|
||||||
|
r.preCaptureOutput(output, pc, false, func() { done <- struct{}{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < pending; i++ {
|
||||||
|
if err := r.ctx.Dispatch(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
default:
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture, withCursor bool, onReady func()) {
|
||||||
|
cursor := int32(0)
|
||||||
|
if withCursor {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := r.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("screencopy capture failed", "err", err)
|
||||||
|
onReady()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create screen buffer failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if withCursor {
|
||||||
|
pc.screenBuf = buf
|
||||||
|
pc.format = e.Format
|
||||||
|
} else {
|
||||||
|
pc.screenBufNoCursor = buf
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create shm pool failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), e.Format)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create wl_buffer failed", "err", err)
|
||||||
|
pool.Destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := frame.Copy(wlBuf); err != nil {
|
||||||
|
log.Error("frame copy failed", "err", err)
|
||||||
|
}
|
||||||
|
pool.Destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
|
||||||
|
if withCursor {
|
||||||
|
pc.yInverted = (e.Flags & 1) != 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||||
|
frame.Destroy()
|
||||||
|
onReady()
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
|
||||||
|
log.Error("screencopy failed")
|
||||||
|
frame.Destroy()
|
||||||
|
onReady()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) createSurfaces() error {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
outputs := make([]*WaylandOutput, 0, len(r.outputs))
|
||||||
|
for _, o := range r.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
|
||||||
|
for _, output := range outputs {
|
||||||
|
os, err := r.createOutputSurface(output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("output %s: %w", output.name, err)
|
||||||
|
}
|
||||||
|
r.surfaces = append(r.surfaces, os)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) createCursor() error {
|
||||||
|
const size = 24
|
||||||
|
const hotspot = size / 2
|
||||||
|
|
||||||
|
surface, err := r.compositor.CreateSurface()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create cursor surface: %w", err)
|
||||||
|
}
|
||||||
|
r.cursorSurface = surface
|
||||||
|
|
||||||
|
buf, err := CreateShmBuffer(size, size, size*4)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create cursor buffer: %w", err)
|
||||||
|
}
|
||||||
|
r.cursorBuffer = buf
|
||||||
|
|
||||||
|
// Draw crosshair
|
||||||
|
data := buf.Data()
|
||||||
|
for y := 0; y < size; y++ {
|
||||||
|
for x := 0; x < size; x++ {
|
||||||
|
off := (y*size + x) * 4
|
||||||
|
// Vertical line
|
||||||
|
if x >= hotspot-1 && x <= hotspot && y >= 2 && y < size-2 {
|
||||||
|
data[off+0] = 255 // B
|
||||||
|
data[off+1] = 255 // G
|
||||||
|
data[off+2] = 255 // R
|
||||||
|
data[off+3] = 255 // A
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Horizontal line
|
||||||
|
if y >= hotspot-1 && y <= hotspot && x >= 2 && x < size-2 {
|
||||||
|
data[off+0] = 255
|
||||||
|
data[off+1] = 255
|
||||||
|
data[off+2] = 255
|
||||||
|
data[off+3] = 255
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Transparent
|
||||||
|
data[off+0] = 0
|
||||||
|
data[off+1] = 0
|
||||||
|
data[off+2] = 0
|
||||||
|
data[off+3] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create cursor pool: %w", err)
|
||||||
|
}
|
||||||
|
r.cursorPool = pool
|
||||||
|
|
||||||
|
wlBuf, err := pool.CreateBuffer(0, size, size, size*4, uint32(FormatARGB8888))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create cursor wl_buffer: %w", err)
|
||||||
|
}
|
||||||
|
r.cursorWlBuf = wlBuf
|
||||||
|
|
||||||
|
if err := surface.Attach(wlBuf, 0, 0); err != nil {
|
||||||
|
return fmt.Errorf("attach cursor: %w", err)
|
||||||
|
}
|
||||||
|
if err := surface.Damage(0, 0, size, size); err != nil {
|
||||||
|
return fmt.Errorf("damage cursor: %w", err)
|
||||||
|
}
|
||||||
|
if err := surface.Commit(); err != nil {
|
||||||
|
return fmt.Errorf("commit cursor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) createOutputSurface(output *WaylandOutput) (*OutputSurface, error) {
|
||||||
|
surface, err := r.compositor.CreateSurface()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create surface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerSurf, err := r.layerShell.GetLayerSurface(
|
||||||
|
surface,
|
||||||
|
output.wlOutput,
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
|
||||||
|
"dms-screenshot",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get layer surface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os := &OutputSurface{
|
||||||
|
output: output,
|
||||||
|
wlSurface: surface,
|
||||||
|
layerSurf: layerSurf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.viewporter != nil {
|
||||||
|
vp, err := r.viewporter.GetViewport(surface)
|
||||||
|
if err == nil {
|
||||||
|
os.viewport = vp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := layerSurf.SetAnchor(
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("set anchor: %w", err)
|
||||||
|
}
|
||||||
|
if err := layerSurf.SetExclusiveZone(-1); err != nil {
|
||||||
|
return nil, fmt.Errorf("set exclusive zone: %w", err)
|
||||||
|
}
|
||||||
|
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
|
||||||
|
return nil, fmt.Errorf("set keyboard interactivity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
|
||||||
|
if err := layerSurf.AckConfigure(e.Serial); err != nil {
|
||||||
|
log.Error("ack configure failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.logicalW = int(e.Width)
|
||||||
|
os.logicalH = int(e.Height)
|
||||||
|
os.configured = true
|
||||||
|
r.captureForSurface(os)
|
||||||
|
r.ensureShortcutsInhibitor(os)
|
||||||
|
})
|
||||||
|
|
||||||
|
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
|
||||||
|
r.running = false
|
||||||
|
r.cancelled = true
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := surface.Commit(); err != nil {
|
||||||
|
return nil, fmt.Errorf("surface commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) ensureShortcutsInhibitor(os *OutputSurface) {
|
||||||
|
if r.shortcutsInhibitMgr == nil || r.seat == nil || r.shortcutsInhibitor != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inhibitor, err := r.shortcutsInhibitMgr.InhibitShortcuts(os.wlSurface, r.seat)
|
||||||
|
if err == nil {
|
||||||
|
r.shortcutsInhibitor = inhibitor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) captureForSurface(os *OutputSurface) {
|
||||||
|
pc := r.preCapture[os.output]
|
||||||
|
if pc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os.screenBuf = pc.screenBuf
|
||||||
|
os.screenBufNoCursor = pc.screenBufNoCursor
|
||||||
|
os.screenFormat = pc.format
|
||||||
|
os.yInverted = pc.yInverted
|
||||||
|
|
||||||
|
r.initRenderBuffer(os)
|
||||||
|
r.applyPreSelection(os)
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) initRenderBuffer(os *OutputSurface) {
|
||||||
|
if os.screenBuf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
slot := &RenderSlot{}
|
||||||
|
|
||||||
|
buf, err := CreateShmBuffer(os.screenBuf.Width, os.screenBuf.Height, os.screenBuf.Stride)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create render slot buffer failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slot.shm = buf
|
||||||
|
|
||||||
|
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create render slot pool failed", "err", err)
|
||||||
|
buf.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slot.pool = pool
|
||||||
|
|
||||||
|
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), os.screenFormat)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create render slot wl_buffer failed", "err", err)
|
||||||
|
pool.Destroy()
|
||||||
|
buf.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slot.wlBuf = wlBuf
|
||||||
|
|
||||||
|
slotRef := slot
|
||||||
|
wlBuf.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
slotRef.busy = false
|
||||||
|
})
|
||||||
|
|
||||||
|
os.slots[i] = slot
|
||||||
|
}
|
||||||
|
os.slotsReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OutputSurface) acquireFreeSlot() *RenderSlot {
|
||||||
|
for _, slot := range os.slots {
|
||||||
|
if slot != nil && !slot.busy {
|
||||||
|
return slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) applyPreSelection(os *OutputSurface) {
|
||||||
|
if r.preSelect.IsEmpty() || os.screenBuf == nil || r.selection.hasSelection {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.preSelect.Output != "" && r.preSelect.Output != os.output.name {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleX := float64(os.logicalW) / float64(os.screenBuf.Width)
|
||||||
|
scaleY := float64(os.logicalH) / float64(os.screenBuf.Height)
|
||||||
|
|
||||||
|
x1 := float64(r.preSelect.X-os.output.x) * scaleX
|
||||||
|
y1 := float64(r.preSelect.Y-os.output.y) * scaleY
|
||||||
|
x2 := float64(r.preSelect.X-os.output.x+r.preSelect.Width) * scaleX
|
||||||
|
y2 := float64(r.preSelect.Y-os.output.y+r.preSelect.Height) * scaleY
|
||||||
|
|
||||||
|
r.selection.hasSelection = true
|
||||||
|
r.selection.dragging = false
|
||||||
|
r.selection.surface = os
|
||||||
|
r.selection.anchorX = x1
|
||||||
|
r.selection.anchorY = y1
|
||||||
|
r.selection.currentX = x2
|
||||||
|
r.selection.currentY = y2
|
||||||
|
r.activeSurface = os
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) getSourceBuffer(os *OutputSurface) *ShmBuffer {
|
||||||
|
if !r.showCapturedCursor && os.screenBufNoCursor != nil {
|
||||||
|
return os.screenBufNoCursor
|
||||||
|
}
|
||||||
|
return os.screenBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) redrawSurface(os *OutputSurface) {
|
||||||
|
srcBuf := r.getSourceBuffer(os)
|
||||||
|
if srcBuf == nil || !os.slotsReady {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slot := os.acquireFreeSlot()
|
||||||
|
if slot == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.shm.CopyFrom(srcBuf)
|
||||||
|
|
||||||
|
// Draw overlay (dimming + selection) into this slot
|
||||||
|
r.drawOverlay(os, slot.shm)
|
||||||
|
|
||||||
|
// Attach and commit (viewport only needs to be set once, but it's cheap)
|
||||||
|
scale := os.output.scale
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.viewport != nil {
|
||||||
|
srcW := float64(slot.shm.Width) / float64(scale)
|
||||||
|
srcH := float64(slot.shm.Height) / float64(scale)
|
||||||
|
_ = os.viewport.SetSource(0, 0, srcW, srcH)
|
||||||
|
_ = os.viewport.SetDestination(int32(os.logicalW), int32(os.logicalH))
|
||||||
|
}
|
||||||
|
_ = os.wlSurface.SetBufferScale(scale)
|
||||||
|
|
||||||
|
_ = os.wlSurface.Attach(slot.wlBuf, 0, 0)
|
||||||
|
_ = os.wlSurface.Damage(0, 0, int32(os.logicalW), int32(os.logicalH))
|
||||||
|
_ = os.wlSurface.Commit()
|
||||||
|
|
||||||
|
// Mark this slot as busy until compositor releases it
|
||||||
|
slot.busy = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) cleanup() {
|
||||||
|
if r.cursorWlBuf != nil {
|
||||||
|
r.cursorWlBuf.Destroy()
|
||||||
|
}
|
||||||
|
if r.cursorPool != nil {
|
||||||
|
r.cursorPool.Destroy()
|
||||||
|
}
|
||||||
|
if r.cursorSurface != nil {
|
||||||
|
r.cursorSurface.Destroy()
|
||||||
|
}
|
||||||
|
if r.cursorBuffer != nil {
|
||||||
|
r.cursorBuffer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
for _, slot := range os.slots {
|
||||||
|
if slot == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slot.wlBuf != nil {
|
||||||
|
slot.wlBuf.Destroy()
|
||||||
|
}
|
||||||
|
if slot.pool != nil {
|
||||||
|
slot.pool.Destroy()
|
||||||
|
}
|
||||||
|
if slot.shm != nil {
|
||||||
|
slot.shm.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if os.viewport != nil {
|
||||||
|
os.viewport.Destroy()
|
||||||
|
}
|
||||||
|
if os.layerSurf != nil {
|
||||||
|
os.layerSurf.Destroy()
|
||||||
|
}
|
||||||
|
if os.wlSurface != nil {
|
||||||
|
os.wlSurface.Destroy()
|
||||||
|
}
|
||||||
|
if os.screenBuf != nil {
|
||||||
|
os.screenBuf.Close()
|
||||||
|
}
|
||||||
|
if os.screenBufNoCursor != nil {
|
||||||
|
os.screenBufNoCursor.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.shortcutsInhibitor != nil {
|
||||||
|
_ = r.shortcutsInhibitor.Destroy()
|
||||||
|
}
|
||||||
|
if r.shortcutsInhibitMgr != nil {
|
||||||
|
_ = r.shortcutsInhibitMgr.Destroy()
|
||||||
|
}
|
||||||
|
if r.viewporter != nil {
|
||||||
|
r.viewporter.Destroy()
|
||||||
|
}
|
||||||
|
if r.screencopy != nil {
|
||||||
|
r.screencopy.Destroy()
|
||||||
|
}
|
||||||
|
if r.pointer != nil {
|
||||||
|
r.pointer.Release()
|
||||||
|
}
|
||||||
|
if r.keyboard != nil {
|
||||||
|
r.keyboard.Release()
|
||||||
|
}
|
||||||
|
if r.display != nil {
|
||||||
|
r.ctx.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
264
core/internal/screenshot/region_input.go
Normal file
264
core/internal/screenshot/region_input.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupInput() {
|
||||||
|
if r.seat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && r.pointer == nil {
|
||||||
|
if pointer, err := r.seat.GetPointer(); err == nil {
|
||||||
|
r.pointer = pointer
|
||||||
|
r.setupPointerHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && r.keyboard == nil {
|
||||||
|
if keyboard, err := r.seat.GetKeyboard(); err == nil {
|
||||||
|
r.keyboard = keyboard
|
||||||
|
r.setupKeyboardHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupPointerHandlers() {
|
||||||
|
r.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
|
||||||
|
if r.cursorSurface != nil {
|
||||||
|
_ = r.pointer.SetCursor(e.Serial, r.cursorSurface, 12, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.activeSurface = nil
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
if os.wlSurface.ID() == e.Surface.ID() {
|
||||||
|
r.activeSurface = os
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.pointerX = e.SurfaceX
|
||||||
|
r.pointerY = e.SurfaceY
|
||||||
|
})
|
||||||
|
|
||||||
|
r.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
|
||||||
|
if r.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.pointerX = e.SurfaceX
|
||||||
|
r.pointerY = e.SurfaceY
|
||||||
|
|
||||||
|
if !r.selection.dragging {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
curX, curY := e.SurfaceX, e.SurfaceY
|
||||||
|
if r.shiftHeld {
|
||||||
|
dx := curX - r.selection.anchorX
|
||||||
|
dy := curY - r.selection.anchorY
|
||||||
|
adx, ady := dx, dy
|
||||||
|
if adx < 0 {
|
||||||
|
adx = -adx
|
||||||
|
}
|
||||||
|
if ady < 0 {
|
||||||
|
ady = -ady
|
||||||
|
}
|
||||||
|
size := adx
|
||||||
|
if ady > adx {
|
||||||
|
size = ady
|
||||||
|
}
|
||||||
|
if dx < 0 {
|
||||||
|
curX = r.selection.anchorX - size
|
||||||
|
} else {
|
||||||
|
curX = r.selection.anchorX + size
|
||||||
|
}
|
||||||
|
if dy < 0 {
|
||||||
|
curY = r.selection.anchorY - size
|
||||||
|
} else {
|
||||||
|
curY = r.selection.anchorY + size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.selection.currentX = curX
|
||||||
|
r.selection.currentY = curY
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
r.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||||
|
if r.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Button {
|
||||||
|
case 0x110: // BTN_LEFT
|
||||||
|
switch e.State {
|
||||||
|
case 1: // pressed
|
||||||
|
r.preSelect = Region{}
|
||||||
|
r.selection.hasSelection = true
|
||||||
|
r.selection.dragging = true
|
||||||
|
r.selection.surface = r.activeSurface
|
||||||
|
r.selection.anchorX = r.pointerX
|
||||||
|
r.selection.anchorY = r.pointerY
|
||||||
|
r.selection.currentX = r.pointerX
|
||||||
|
r.selection.currentY = r.pointerY
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
case 0: // released
|
||||||
|
r.selection.dragging = false
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
r.cancelled = true
|
||||||
|
r.running = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupKeyboardHandlers() {
|
||||||
|
r.keyboard.SetModifiersHandler(func(e client.KeyboardModifiersEvent) {
|
||||||
|
r.shiftHeld = e.ModsDepressed&1 != 0
|
||||||
|
})
|
||||||
|
|
||||||
|
r.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||||
|
if e.State != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Key {
|
||||||
|
case 1:
|
||||||
|
r.cancelled = true
|
||||||
|
r.running = false
|
||||||
|
case 25:
|
||||||
|
r.showCapturedCursor = !r.showCapturedCursor
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
case 28, 57:
|
||||||
|
if r.selection.hasSelection {
|
||||||
|
r.finishSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) finishSelection() {
|
||||||
|
if r.selection.surface == nil {
|
||||||
|
r.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os := r.selection.surface
|
||||||
|
srcBuf := r.getSourceBuffer(os)
|
||||||
|
if srcBuf == nil {
|
||||||
|
r.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
x1, y1 := r.selection.anchorX, r.selection.anchorY
|
||||||
|
x2, y2 := r.selection.currentX, r.selection.currentY
|
||||||
|
|
||||||
|
if x1 > x2 {
|
||||||
|
x1, x2 = x2, x1
|
||||||
|
}
|
||||||
|
if y1 > y2 {
|
||||||
|
y1, y2 = y2, y1
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleX, scaleY := 1.0, 1.0
|
||||||
|
if os.logicalW > 0 {
|
||||||
|
scaleX = float64(srcBuf.Width) / float64(os.logicalW)
|
||||||
|
scaleY = float64(srcBuf.Height) / float64(os.logicalH)
|
||||||
|
}
|
||||||
|
|
||||||
|
bx1 := int(x1 * scaleX)
|
||||||
|
by1 := int(y1 * scaleY)
|
||||||
|
bx2 := int(x2 * scaleX)
|
||||||
|
by2 := int(y2 * scaleY)
|
||||||
|
|
||||||
|
// Clamp to buffer bounds
|
||||||
|
if bx1 < 0 {
|
||||||
|
bx1 = 0
|
||||||
|
}
|
||||||
|
if by1 < 0 {
|
||||||
|
by1 = 0
|
||||||
|
}
|
||||||
|
if bx2 > srcBuf.Width {
|
||||||
|
bx2 = srcBuf.Width
|
||||||
|
}
|
||||||
|
if by2 > srcBuf.Height {
|
||||||
|
by2 = srcBuf.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
w, h := bx2-bx1+1, by2-by1+1
|
||||||
|
if r.shiftHeld && w != h {
|
||||||
|
if w < h {
|
||||||
|
h = w
|
||||||
|
} else {
|
||||||
|
w = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w < 1 {
|
||||||
|
w = 1
|
||||||
|
}
|
||||||
|
if h < 1 {
|
||||||
|
h = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cropped buffer and copy pixels directly
|
||||||
|
cropped, err := CreateShmBuffer(w, h, w*4)
|
||||||
|
if err != nil {
|
||||||
|
r.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
srcData := srcBuf.Data()
|
||||||
|
dstData := cropped.Data()
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
srcY := by1 + y
|
||||||
|
if srcY >= srcBuf.Height {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
srcX := bx1 + x
|
||||||
|
if srcX >= srcBuf.Width {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
si := srcY*srcBuf.Stride + srcX*4
|
||||||
|
di := y*cropped.Stride + x*4
|
||||||
|
if si+3 < len(srcData) && di+3 < len(dstData) {
|
||||||
|
dstData[di+0] = srcData[si+0]
|
||||||
|
dstData[di+1] = srcData[si+1]
|
||||||
|
dstData[di+2] = srcData[si+2]
|
||||||
|
dstData[di+3] = srcData[si+3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.capturedBuffer = cropped
|
||||||
|
r.capturedRegion = Region{
|
||||||
|
X: int32(bx1),
|
||||||
|
Y: int32(by1),
|
||||||
|
Width: int32(w),
|
||||||
|
Height: int32(h),
|
||||||
|
Output: os.output.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also store for "last region" feature with global coords
|
||||||
|
r.result = Region{
|
||||||
|
X: int32(bx1) + os.output.x,
|
||||||
|
Y: int32(by1) + os.output.y,
|
||||||
|
Width: int32(w),
|
||||||
|
Height: int32(h),
|
||||||
|
Output: os.output.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.running = false
|
||||||
|
}
|
||||||
322
core/internal/screenshot/region_render.go
Normal file
322
core/internal/screenshot/region_render.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var fontGlyphs = map[rune][12]uint8{
|
||||||
|
'0': {0x3C, 0x66, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'1': {0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00},
|
||||||
|
'2': {0x3C, 0x66, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x66, 0x7E, 0x00, 0x00},
|
||||||
|
'3': {0x3C, 0x66, 0x06, 0x06, 0x1C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'4': {0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xCC, 0xFE, 0x0C, 0x0C, 0x1E, 0x00, 0x00},
|
||||||
|
'5': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'6': {0x1C, 0x30, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'7': {0x7E, 0x66, 0x06, 0x06, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00},
|
||||||
|
'8': {0x3C, 0x66, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'9': {0x3C, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x0C, 0x38, 0x00, 0x00},
|
||||||
|
'x': {0x00, 0x00, 0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00, 0x00},
|
||||||
|
'E': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00, 0x00},
|
||||||
|
'P': {0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00},
|
||||||
|
'S': {0x3C, 0x66, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'a': {0x00, 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
|
||||||
|
'c': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'd': {0x00, 0x00, 0x06, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
|
||||||
|
'e': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x7E, 0x60, 0x60, 0x3C, 0x00, 0x00},
|
||||||
|
'h': {0x00, 0x60, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00},
|
||||||
|
'i': {0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00},
|
||||||
|
'n': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00},
|
||||||
|
'o': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||||
|
'p': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x00, 0x00},
|
||||||
|
'r': {0x00, 0x00, 0x00, 0x6E, 0x76, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00},
|
||||||
|
's': {0x00, 0x00, 0x00, 0x3E, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x7C, 0x00, 0x00},
|
||||||
|
't': {0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00},
|
||||||
|
'u': {0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
|
||||||
|
'w': {0x00, 0x00, 0x00, 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00, 0x00},
|
||||||
|
'l': {0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00},
|
||||||
|
' ': {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
':': {0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00},
|
||||||
|
'/': {0x00, 0x02, 0x06, 0x0C, 0x18, 0x18, 0x30, 0x60, 0x40, 0x00, 0x00, 0x00},
|
||||||
|
'[': {0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00},
|
||||||
|
']': {0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00},
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverlayStyle struct {
|
||||||
|
BackgroundR, BackgroundG, BackgroundB, BackgroundA uint8
|
||||||
|
TextR, TextG, TextB uint8
|
||||||
|
AccentR, AccentG, AccentB uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultOverlayStyle = OverlayStyle{
|
||||||
|
BackgroundR: 30, BackgroundG: 30, BackgroundB: 30, BackgroundA: 220,
|
||||||
|
TextR: 255, TextG: 255, TextB: 255,
|
||||||
|
AccentR: 100, AccentG: 180, AccentB: 255,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawOverlay(os *OutputSurface, renderBuf *ShmBuffer) {
|
||||||
|
data := renderBuf.Data()
|
||||||
|
stride := renderBuf.Stride
|
||||||
|
w, h := renderBuf.Width, renderBuf.Height
|
||||||
|
format := os.screenFormat
|
||||||
|
|
||||||
|
// Dim the entire buffer
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
off := y * stride
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
i := off + x*4
|
||||||
|
if i+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data[i+0] = uint8(int(data[i+0]) * 3 / 5)
|
||||||
|
data[i+1] = uint8(int(data[i+1]) * 3 / 5)
|
||||||
|
data[i+2] = uint8(int(data[i+2]) * 3 / 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.drawHUD(data, stride, w, h, format)
|
||||||
|
|
||||||
|
if !r.selection.hasSelection || r.selection.surface != os {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleX := float64(w) / float64(os.logicalW)
|
||||||
|
scaleY := float64(h) / float64(os.logicalH)
|
||||||
|
|
||||||
|
bx1 := int(r.selection.anchorX * scaleX)
|
||||||
|
by1 := int(r.selection.anchorY * scaleY)
|
||||||
|
bx2 := int(r.selection.currentX * scaleX)
|
||||||
|
by2 := int(r.selection.currentY * scaleY)
|
||||||
|
|
||||||
|
if bx1 > bx2 {
|
||||||
|
bx1, bx2 = bx2, bx1
|
||||||
|
}
|
||||||
|
if by1 > by2 {
|
||||||
|
by1, by2 = by2, by1
|
||||||
|
}
|
||||||
|
|
||||||
|
bx1 = clamp(bx1, 0, w-1)
|
||||||
|
by1 = clamp(by1, 0, h-1)
|
||||||
|
bx2 = clamp(bx2, 0, w-1)
|
||||||
|
by2 = clamp(by2, 0, h-1)
|
||||||
|
|
||||||
|
srcBuf := r.getSourceBuffer(os)
|
||||||
|
srcData := srcBuf.Data()
|
||||||
|
for y := by1; y <= by2; y++ {
|
||||||
|
rowOff := y * stride
|
||||||
|
for x := bx1; x <= bx2; x++ {
|
||||||
|
si := y*srcBuf.Stride + x*4
|
||||||
|
di := rowOff + x*4
|
||||||
|
if si+3 >= len(srcData) || di+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data[di+0] = srcData[si+0]
|
||||||
|
data[di+1] = srcData[si+1]
|
||||||
|
data[di+2] = srcData[si+2]
|
||||||
|
data[di+3] = srcData[si+3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selW, selH := bx2-bx1+1, by2-by1+1
|
||||||
|
if r.shiftHeld && selW != selH {
|
||||||
|
if selW < selH {
|
||||||
|
selH = selW
|
||||||
|
} else {
|
||||||
|
selW = selH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.drawBorder(data, stride, w, h, bx1, by1, selW, selH, format)
|
||||||
|
r.drawDimensions(data, stride, w, h, bx1, by1, selW, selH, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32) {
|
||||||
|
if r.selection.dragging {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
style := LoadOverlayStyle()
|
||||||
|
const charW, charH, padding, itemSpacing = 8, 12, 12, 24
|
||||||
|
|
||||||
|
cursorLabel := "hide"
|
||||||
|
if !r.showCapturedCursor {
|
||||||
|
cursorLabel = "show"
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []struct{ key, desc string }{
|
||||||
|
{"Space/Enter", "capture"},
|
||||||
|
{"P", cursorLabel + " cursor"},
|
||||||
|
{"Esc", "cancel"},
|
||||||
|
}
|
||||||
|
|
||||||
|
totalW := 0
|
||||||
|
for i, item := range items {
|
||||||
|
totalW += len(item.key)*(charW+1) + 4 + len(item.desc)*(charW+1)
|
||||||
|
if i < len(items)-1 {
|
||||||
|
totalW += itemSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hudW := totalW + padding*2
|
||||||
|
hudH := charH + padding*2
|
||||||
|
hudX := (bufW - hudW) / 2
|
||||||
|
hudY := bufH - hudH - 20
|
||||||
|
|
||||||
|
r.fillRect(data, stride, bufW, bufH, hudX, hudY, hudW, hudH,
|
||||||
|
style.BackgroundR, style.BackgroundG, style.BackgroundB, style.BackgroundA, format)
|
||||||
|
|
||||||
|
tx, ty := hudX+padding, hudY+padding
|
||||||
|
for i, item := range items {
|
||||||
|
r.drawText(data, stride, bufW, bufH, tx, ty, item.key,
|
||||||
|
style.AccentR, style.AccentG, style.AccentB, format)
|
||||||
|
tx += len(item.key) * (charW + 1)
|
||||||
|
|
||||||
|
r.drawText(data, stride, bufW, bufH, tx, ty, " "+item.desc,
|
||||||
|
style.TextR, style.TextG, style.TextB, format)
|
||||||
|
tx += (1 + len(item.desc)) * (charW + 1)
|
||||||
|
|
||||||
|
if i < len(items)-1 {
|
||||||
|
tx += itemSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawBorder(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) {
|
||||||
|
const thickness = 2
|
||||||
|
for i := 0; i < thickness; i++ {
|
||||||
|
r.drawHLine(data, stride, bufW, bufH, x-i, y-i, w+2*i, format)
|
||||||
|
r.drawHLine(data, stride, bufW, bufH, x-i, y+h+i-1, w+2*i, format)
|
||||||
|
r.drawVLine(data, stride, bufW, bufH, x-i, y-i, h+2*i, format)
|
||||||
|
r.drawVLine(data, stride, bufW, bufH, x+w+i-1, y-i, h+2*i, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawHLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) {
|
||||||
|
if y < 0 || y >= bufH {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rowOff := y * stride
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
px := x + i
|
||||||
|
if px < 0 || px >= bufW {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
off := rowOff + px*4
|
||||||
|
if off+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawVLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) {
|
||||||
|
if x < 0 || x >= bufW {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
py := y + i
|
||||||
|
if py < 0 || py >= bufH {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
off := py*stride + x*4
|
||||||
|
if off+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawDimensions(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) {
|
||||||
|
text := fmt.Sprintf("%dx%d", w, h)
|
||||||
|
|
||||||
|
const charW, charH = 8, 12
|
||||||
|
textW := len(text) * (charW + 1)
|
||||||
|
textH := charH
|
||||||
|
|
||||||
|
tx := x + (w-textW)/2
|
||||||
|
ty := y + h + 8
|
||||||
|
|
||||||
|
if ty+textH > bufH {
|
||||||
|
ty = y - textH - 8
|
||||||
|
}
|
||||||
|
tx = clamp(tx, 0, bufW-textW)
|
||||||
|
|
||||||
|
r.fillRect(data, stride, bufW, bufH, tx-4, ty-2, textW+8, textH+4, 0, 0, 0, 200, format)
|
||||||
|
r.drawText(data, stride, bufW, bufH, tx, ty, text, 255, 255, 255, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) fillRect(data []byte, stride, bufW, bufH, x, y, w, h int, cr, cg, cb, ca uint8, format uint32) {
|
||||||
|
alpha := float64(ca) / 255.0
|
||||||
|
invAlpha := 1.0 - alpha
|
||||||
|
|
||||||
|
c0, c2 := cb, cr
|
||||||
|
if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) {
|
||||||
|
c0, c2 = cr, cb
|
||||||
|
}
|
||||||
|
|
||||||
|
for py := y; py < y+h && py < bufH; py++ {
|
||||||
|
if py < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for px := x; px < x+w && px < bufW; px++ {
|
||||||
|
if px < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
off := py*stride + px*4
|
||||||
|
if off+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data[off+0] = uint8(float64(data[off+0])*invAlpha + float64(c0)*alpha)
|
||||||
|
data[off+1] = uint8(float64(data[off+1])*invAlpha + float64(cg)*alpha)
|
||||||
|
data[off+2] = uint8(float64(data[off+2])*invAlpha + float64(c2)*alpha)
|
||||||
|
data[off+3] = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawText(data []byte, stride, bufW, bufH, x, y int, text string, cr, cg, cb uint8, format uint32) {
|
||||||
|
for i, ch := range text {
|
||||||
|
r.drawChar(data, stride, bufW, bufH, x+i*9, y, ch, cr, cg, cb, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) drawChar(data []byte, stride, bufW, bufH, x, y int, ch rune, cr, cg, cb uint8, format uint32) {
|
||||||
|
glyph, ok := fontGlyphs[ch]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c0, c2 := cb, cr
|
||||||
|
if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) {
|
||||||
|
c0, c2 = cr, cb
|
||||||
|
}
|
||||||
|
|
||||||
|
for row := 0; row < 12; row++ {
|
||||||
|
py := y + row
|
||||||
|
if py < 0 || py >= bufH {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bits := glyph[row]
|
||||||
|
for col := 0; col < 8; col++ {
|
||||||
|
if (bits & (1 << (7 - col))) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
px := x + col
|
||||||
|
if px < 0 || px >= bufW {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
off := py*stride + px*4
|
||||||
|
if off+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data[off], data[off+1], data[off+2], data[off+3] = c0, cg, c2, 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp(v, lo, hi int) int {
|
||||||
|
switch {
|
||||||
|
case v < lo:
|
||||||
|
return lo
|
||||||
|
case v > hi:
|
||||||
|
return hi
|
||||||
|
default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
613
core/internal/screenshot/screenshot.go
Normal file
613
core/internal/screenshot/screenshot.go
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WaylandOutput struct {
|
||||||
|
wlOutput *client.Output
|
||||||
|
globalName uint32
|
||||||
|
name string
|
||||||
|
x, y int32
|
||||||
|
width int32
|
||||||
|
height int32
|
||||||
|
scale int32
|
||||||
|
transform int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptureResult struct {
|
||||||
|
Buffer *ShmBuffer
|
||||||
|
Region Region
|
||||||
|
YInverted bool
|
||||||
|
Format uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Screenshoter struct {
|
||||||
|
config Config
|
||||||
|
|
||||||
|
display *client.Display
|
||||||
|
registry *client.Registry
|
||||||
|
ctx *client.Context
|
||||||
|
|
||||||
|
compositor *client.Compositor
|
||||||
|
shm *client.Shm
|
||||||
|
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||||
|
|
||||||
|
outputs map[uint32]*WaylandOutput
|
||||||
|
outputsMu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Screenshoter {
|
||||||
|
return &Screenshoter{
|
||||||
|
config: config,
|
||||||
|
outputs: make(map[uint32]*WaylandOutput),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) Run() (*CaptureResult, error) {
|
||||||
|
if err := s.connect(); err != nil {
|
||||||
|
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer s.cleanup()
|
||||||
|
|
||||||
|
if err := s.setupRegistry(); err != nil {
|
||||||
|
return nil, fmt.Errorf("registry setup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.screencopy == nil {
|
||||||
|
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.config.Mode {
|
||||||
|
case ModeLastRegion:
|
||||||
|
return s.captureLastRegion()
|
||||||
|
case ModeRegion:
|
||||||
|
return s.captureRegion()
|
||||||
|
case ModeWindow:
|
||||||
|
return s.captureWindow()
|
||||||
|
case ModeOutput:
|
||||||
|
return s.captureOutput(s.config.OutputName)
|
||||||
|
case ModeFullScreen:
|
||||||
|
return s.captureFullScreen()
|
||||||
|
case ModeAllScreens:
|
||||||
|
return s.captureAllScreens()
|
||||||
|
default:
|
||||||
|
return s.captureRegion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
||||||
|
lastRegion := GetLastRegion()
|
||||||
|
if lastRegion.IsEmpty() {
|
||||||
|
return s.captureRegion()
|
||||||
|
}
|
||||||
|
|
||||||
|
output := s.findOutputForRegion(lastRegion)
|
||||||
|
if output == nil {
|
||||||
|
return s.captureRegion()
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.captureRegionOnOutput(output, lastRegion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||||
|
selector := NewRegionSelector(s)
|
||||||
|
result, cancelled, err := selector.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("region selection: %w", err)
|
||||||
|
}
|
||||||
|
if cancelled || result == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := SaveLastRegion(result.Region); err != nil {
|
||||||
|
log.Debug("failed to save last region", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
|
||||||
|
geom, err := GetActiveWindow()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
region := Region{
|
||||||
|
X: geom.X,
|
||||||
|
Y: geom.Y,
|
||||||
|
Width: geom.Width,
|
||||||
|
Height: geom.Height,
|
||||||
|
}
|
||||||
|
|
||||||
|
output := s.findOutputForRegion(region)
|
||||||
|
if output == nil {
|
||||||
|
return nil, fmt.Errorf("could not find output for window")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.captureRegionOnOutput(output, region)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
|
||||||
|
output := s.findFocusedOutput()
|
||||||
|
if output == nil {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
output = o
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if output == nil {
|
||||||
|
return nil, fmt.Errorf("no output available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.captureWholeOutput(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureOutput(name string) (*CaptureResult, error) {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
var output *WaylandOutput
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
if o.name == name {
|
||||||
|
output = o
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
|
||||||
|
if output == nil {
|
||||||
|
return nil, fmt.Errorf("output %q not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.captureWholeOutput(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
outputs := make([]*WaylandOutput, 0, len(s.outputs))
|
||||||
|
var minX, minY, maxX, maxY int32
|
||||||
|
first := true
|
||||||
|
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
right := o.x + o.width
|
||||||
|
bottom := o.y + o.height
|
||||||
|
|
||||||
|
if first {
|
||||||
|
minX, minY = o.x, o.y
|
||||||
|
maxX, maxY = right, bottom
|
||||||
|
first = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.x < minX {
|
||||||
|
minX = o.x
|
||||||
|
}
|
||||||
|
if o.y < minY {
|
||||||
|
minY = o.y
|
||||||
|
}
|
||||||
|
if right > maxX {
|
||||||
|
maxX = right
|
||||||
|
}
|
||||||
|
if bottom > maxY {
|
||||||
|
maxY = bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
|
||||||
|
if len(outputs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no outputs available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outputs) == 1 {
|
||||||
|
return s.captureWholeOutput(outputs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
totalW := maxX - minX
|
||||||
|
totalH := maxY - minY
|
||||||
|
|
||||||
|
compositeStride := int(totalW) * 4
|
||||||
|
composite, err := CreateShmBuffer(int(totalW), int(totalH), compositeStride)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
composite.Clear()
|
||||||
|
|
||||||
|
var format uint32
|
||||||
|
for _, output := range outputs {
|
||||||
|
result, err := s.captureWholeOutput(output)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("failed to capture output", "name", output.name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == 0 {
|
||||||
|
format = result.Format
|
||||||
|
}
|
||||||
|
s.blitBuffer(composite, result.Buffer, int(output.x-minX), int(output.y-minY), result.YInverted)
|
||||||
|
result.Buffer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: composite,
|
||||||
|
Region: Region{X: minX, Y: minY, Width: totalW, Height: totalH},
|
||||||
|
Format: format,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
||||||
|
srcData := src.Data()
|
||||||
|
dstData := dst.Data()
|
||||||
|
|
||||||
|
for srcY := 0; srcY < src.Height; srcY++ {
|
||||||
|
actualSrcY := srcY
|
||||||
|
if yInverted {
|
||||||
|
actualSrcY = src.Height - 1 - srcY
|
||||||
|
}
|
||||||
|
|
||||||
|
dy := dstY + srcY
|
||||||
|
if dy < 0 || dy >= dst.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcRowOff := actualSrcY * src.Stride
|
||||||
|
dstRowOff := dy * dst.Stride
|
||||||
|
|
||||||
|
for srcX := 0; srcX < src.Width; srcX++ {
|
||||||
|
dx := dstX + srcX
|
||||||
|
if dx < 0 || dx >= dst.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
si := srcRowOff + srcX*4
|
||||||
|
di := dstRowOff + dx*4
|
||||||
|
|
||||||
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dstData[di+0] = srcData[si+0]
|
||||||
|
dstData[di+1] = srcData[si+1]
|
||||||
|
dstData[di+2] = srcData[si+2]
|
||||||
|
dstData[di+3] = srcData[si+3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
|
||||||
|
cursor := int32(0)
|
||||||
|
if s.config.IncludeCursor {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("capture output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.processFrame(frame, Region{
|
||||||
|
X: output.x,
|
||||||
|
Y: output.y,
|
||||||
|
Width: output.width,
|
||||||
|
Height: output.height,
|
||||||
|
Output: output.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||||
|
localX := region.X - output.x
|
||||||
|
localY := region.Y - output.y
|
||||||
|
|
||||||
|
cursor := int32(0)
|
||||||
|
if s.config.IncludeCursor {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := s.screencopy.CaptureOutputRegion(
|
||||||
|
cursor,
|
||||||
|
output.wlOutput,
|
||||||
|
localX, localY,
|
||||||
|
region.Width, region.Height,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("capture region: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.processFrame(frame, region)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
|
||||||
|
var buf *ShmBuffer
|
||||||
|
var format PixelFormat
|
||||||
|
var yInverted bool
|
||||||
|
ready := false
|
||||||
|
failed := false
|
||||||
|
|
||||||
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
var err error
|
||||||
|
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to create buffer", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
format = PixelFormat(e.Format)
|
||||||
|
buf.Format = format
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
|
||||||
|
yInverted = (e.Flags & 1) != 0
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
|
||||||
|
if buf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := s.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to create pool", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), uint32(format))
|
||||||
|
if err != nil {
|
||||||
|
pool.Destroy()
|
||||||
|
log.Error("failed to create wl_buffer", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := frame.Copy(wlBuf); err != nil {
|
||||||
|
log.Error("failed to copy frame", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||||
|
ready = true
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
|
||||||
|
failed = true
|
||||||
|
})
|
||||||
|
|
||||||
|
for !ready && !failed {
|
||||||
|
if err := s.ctx.Dispatch(); err != nil {
|
||||||
|
frame.Destroy()
|
||||||
|
return nil, fmt.Errorf("dispatch: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.Destroy()
|
||||||
|
|
||||||
|
if failed {
|
||||||
|
if buf != nil {
|
||||||
|
buf.Close()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("frame capture failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: buf,
|
||||||
|
Region: region,
|
||||||
|
YInverted: yInverted,
|
||||||
|
Format: uint32(format),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
defer s.outputsMu.Unlock()
|
||||||
|
|
||||||
|
cx := region.X + region.Width/2
|
||||||
|
cy := region.Y + region.Height/2
|
||||||
|
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
if cx >= o.x && cx < o.x+o.width && cy >= o.y && cy < o.y+o.height {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
if region.X >= o.x && region.X < o.x+o.width &&
|
||||||
|
region.Y >= o.y && region.Y < o.y+o.height {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) findFocusedOutput() *WaylandOutput {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
defer s.outputsMu.Unlock()
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) connect() error {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.display = display
|
||||||
|
s.ctx = display.Context()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) roundtrip() error {
|
||||||
|
return wlhelpers.Roundtrip(s.display, s.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) setupRegistry() error {
|
||||||
|
registry, err := s.display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.registry = registry
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
s.handleGlobal(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
delete(s.outputs, e.Name)
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) handleGlobal(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case client.CompositorInterfaceName:
|
||||||
|
comp := client.NewCompositor(s.ctx)
|
||||||
|
if err := s.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
|
||||||
|
s.compositor = comp
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.ShmInterfaceName:
|
||||||
|
shm := client.NewShm(s.ctx)
|
||||||
|
if err := s.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||||
|
s.shm = shm
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
output := client.NewOutput(s.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := s.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
s.outputs[e.Name] = &WaylandOutput{
|
||||||
|
wlOutput: output,
|
||||||
|
globalName: e.Name,
|
||||||
|
scale: 1,
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
s.setupOutputHandlers(e.Name, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
|
sc := wlr_screencopy.NewZwlrScreencopyManagerV1(s.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 3 {
|
||||||
|
version = 3
|
||||||
|
}
|
||||||
|
if err := s.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
|
||||||
|
s.screencopy = sc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) setupOutputHandlers(name uint32, output *client.Output) {
|
||||||
|
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
if o, ok := s.outputs[name]; ok {
|
||||||
|
o.x, o.y = e.X, e.Y
|
||||||
|
o.transform = int32(e.Transform)
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||||
|
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
if o, ok := s.outputs[name]; ok {
|
||||||
|
o.width, o.height = e.Width, e.Height
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
if o, ok := s.outputs[name]; ok {
|
||||||
|
o.scale = e.Factor
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
if o, ok := s.outputs[name]; ok {
|
||||||
|
o.name = e.Name
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) cleanup() {
|
||||||
|
if s.screencopy != nil {
|
||||||
|
s.screencopy.Destroy()
|
||||||
|
}
|
||||||
|
if s.display != nil {
|
||||||
|
s.ctx.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) GetOutputs() []*WaylandOutput {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
defer s.outputsMu.Unlock()
|
||||||
|
out := make([]*WaylandOutput, 0, len(s.outputs))
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
out = append(out, o)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListOutputs() ([]Output, error) {
|
||||||
|
sc := New(DefaultConfig())
|
||||||
|
if err := sc.connect(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer sc.cleanup()
|
||||||
|
|
||||||
|
if err := sc.setupRegistry(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := sc.roundtrip(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := sc.roundtrip(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.outputsMu.Lock()
|
||||||
|
defer sc.outputsMu.Unlock()
|
||||||
|
|
||||||
|
result := make([]Output, 0, len(sc.outputs))
|
||||||
|
for _, o := range sc.outputs {
|
||||||
|
result = append(result, Output{
|
||||||
|
Name: o.name,
|
||||||
|
X: o.x,
|
||||||
|
Y: o.y,
|
||||||
|
Width: o.width,
|
||||||
|
Height: o.height,
|
||||||
|
Scale: o.scale,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
18
core/internal/screenshot/shm.go
Normal file
18
core/internal/screenshot/shm.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
|
|
||||||
|
type PixelFormat = shm.PixelFormat
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatARGB8888 = shm.FormatARGB8888
|
||||||
|
FormatXRGB8888 = shm.FormatXRGB8888
|
||||||
|
FormatABGR8888 = shm.FormatABGR8888
|
||||||
|
FormatXBGR8888 = shm.FormatXBGR8888
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShmBuffer = shm.Buffer
|
||||||
|
|
||||||
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
|
return shm.CreateBuffer(width, height, stride)
|
||||||
|
}
|
||||||
65
core/internal/screenshot/state.go
Normal file
65
core/internal/screenshot/state.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PersistentState struct {
|
||||||
|
LastRegion Region `json:"last_region"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStateFilePath() string {
|
||||||
|
cacheDir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
cacheDir = path.Join(os.Getenv("HOME"), ".cache")
|
||||||
|
}
|
||||||
|
return filepath.Join(cacheDir, "dms", "screenshot-state.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadState() (*PersistentState, error) {
|
||||||
|
path := getStateFilePath()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &PersistentState{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var state PersistentState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
return &PersistentState{}, nil
|
||||||
|
}
|
||||||
|
return &state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveState(state *PersistentState) error {
|
||||||
|
path := getStateFilePath()
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLastRegion() Region {
|
||||||
|
state, err := LoadState()
|
||||||
|
if err != nil {
|
||||||
|
return Region{}
|
||||||
|
}
|
||||||
|
return state.LastRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveLastRegion(r Region) error {
|
||||||
|
state, _ := LoadState()
|
||||||
|
state.LastRegion = r
|
||||||
|
return SaveState(state)
|
||||||
|
}
|
||||||
127
core/internal/screenshot/theme.go
Normal file
127
core/internal/screenshot/theme.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ThemeColors struct {
|
||||||
|
Background string `json:"surface"`
|
||||||
|
OnSurface string `json:"on_surface"`
|
||||||
|
Primary string `json:"primary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColorScheme struct {
|
||||||
|
Dark ThemeColors `json:"dark"`
|
||||||
|
Light ThemeColors `json:"light"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColorsFile struct {
|
||||||
|
Colors ColorScheme `json:"colors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedStyle *OverlayStyle
|
||||||
|
|
||||||
|
func LoadOverlayStyle() OverlayStyle {
|
||||||
|
if cachedStyle != nil {
|
||||||
|
return *cachedStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
style := DefaultOverlayStyle
|
||||||
|
colors := loadColorsFile()
|
||||||
|
if colors == nil {
|
||||||
|
cachedStyle = &style
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
theme := &colors.Dark
|
||||||
|
if isLightMode() {
|
||||||
|
theme = &colors.Light
|
||||||
|
}
|
||||||
|
|
||||||
|
if bg, ok := parseHexColor(theme.Background); ok {
|
||||||
|
style.BackgroundR, style.BackgroundG, style.BackgroundB = bg[0], bg[1], bg[2]
|
||||||
|
}
|
||||||
|
if text, ok := parseHexColor(theme.OnSurface); ok {
|
||||||
|
style.TextR, style.TextG, style.TextB = text[0], text[1], text[2]
|
||||||
|
}
|
||||||
|
if accent, ok := parseHexColor(theme.Primary); ok {
|
||||||
|
style.AccentR, style.AccentG, style.AccentB = accent[0], accent[1], accent[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedStyle = &style
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadColorsFile() *ColorScheme {
|
||||||
|
path := getColorsFilePath()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var file ColorsFile
|
||||||
|
if err := json.Unmarshal(data, &file); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &file.Colors
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColorsFilePath() string {
|
||||||
|
cacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
if cacheDir == "" {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cacheDir = filepath.Join(home, ".cache")
|
||||||
|
}
|
||||||
|
return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLightMode() bool {
|
||||||
|
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme := strings.TrimSpace(string(out))
|
||||||
|
switch scheme {
|
||||||
|
case "'prefer-light'", "'default'":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHexColor(hex string) ([3]uint8, bool) {
|
||||||
|
hex = strings.TrimPrefix(hex, "#")
|
||||||
|
if len(hex) != 6 {
|
||||||
|
return [3]uint8{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var r, g, b uint8
|
||||||
|
for i, ptr := range []*uint8{&r, &g, &b} {
|
||||||
|
val := 0
|
||||||
|
for j := 0; j < 2; j++ {
|
||||||
|
c := hex[i*2+j]
|
||||||
|
val *= 16
|
||||||
|
switch {
|
||||||
|
case c >= '0' && c <= '9':
|
||||||
|
val += int(c - '0')
|
||||||
|
case c >= 'a' && c <= 'f':
|
||||||
|
val += int(c - 'a' + 10)
|
||||||
|
case c >= 'A' && c <= 'F':
|
||||||
|
val += int(c - 'A' + 10)
|
||||||
|
default:
|
||||||
|
return [3]uint8{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*ptr = uint8(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [3]uint8{r, g, b}, true
|
||||||
|
}
|
||||||
68
core/internal/screenshot/types.go
Normal file
68
core/internal/screenshot/types.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
type Mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeRegion Mode = iota
|
||||||
|
ModeWindow
|
||||||
|
ModeFullScreen
|
||||||
|
ModeAllScreens
|
||||||
|
ModeOutput
|
||||||
|
ModeLastRegion
|
||||||
|
)
|
||||||
|
|
||||||
|
type Format int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatPNG Format = iota
|
||||||
|
FormatJPEG
|
||||||
|
FormatPPM
|
||||||
|
)
|
||||||
|
|
||||||
|
type Region struct {
|
||||||
|
X int32 `json:"x"`
|
||||||
|
Y int32 `json:"y"`
|
||||||
|
Width int32 `json:"width"`
|
||||||
|
Height int32 `json:"height"`
|
||||||
|
Output string `json:"output,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Region) IsEmpty() bool {
|
||||||
|
return r.Width <= 0 || r.Height <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Output struct {
|
||||||
|
Name string
|
||||||
|
X, Y int32
|
||||||
|
Width int32
|
||||||
|
Height int32
|
||||||
|
Scale int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Mode Mode
|
||||||
|
OutputName string
|
||||||
|
IncludeCursor bool
|
||||||
|
Format Format
|
||||||
|
Quality int
|
||||||
|
OutputDir string
|
||||||
|
Filename string
|
||||||
|
Clipboard bool
|
||||||
|
SaveFile bool
|
||||||
|
Notify bool
|
||||||
|
Stdout bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Mode: ModeRegion,
|
||||||
|
IncludeCursor: false,
|
||||||
|
Format: FormatPNG,
|
||||||
|
Quality: 90,
|
||||||
|
OutputDir: "",
|
||||||
|
Filename: "",
|
||||||
|
Clipboard: true,
|
||||||
|
SaveFile: true,
|
||||||
|
Notify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
64
core/internal/server/apppicker/handlers.go
Normal file
64
core/internal/server/apppicker/handlers.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package apppicker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params map[string]any `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
|
||||||
|
switch req.Method {
|
||||||
|
case "apppicker.open", "browser.open":
|
||||||
|
handleOpen(conn, req, manager)
|
||||||
|
default:
|
||||||
|
models.RespondError(conn, req.ID, "unknown method")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleOpen(conn net.Conn, req Request, manager *Manager) {
|
||||||
|
log.Infof("AppPicker: Received %s request with params: %+v", req.Method, req.Params)
|
||||||
|
|
||||||
|
target, ok := req.Params["target"].(string)
|
||||||
|
if !ok {
|
||||||
|
target, ok = req.Params["url"].(string)
|
||||||
|
if !ok {
|
||||||
|
log.Warnf("AppPicker: Invalid target parameter in request")
|
||||||
|
models.RespondError(conn, req.ID, "invalid target parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event := OpenEvent{
|
||||||
|
Target: target,
|
||||||
|
RequestType: "url",
|
||||||
|
}
|
||||||
|
|
||||||
|
if mimeType, ok := req.Params["mimeType"].(string); ok {
|
||||||
|
event.MimeType = mimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
if categories, ok := req.Params["categories"].([]any); ok {
|
||||||
|
event.Categories = make([]string, 0, len(categories))
|
||||||
|
for _, cat := range categories {
|
||||||
|
if catStr, ok := cat.(string); ok {
|
||||||
|
event.Categories = append(event.Categories, catStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestType, ok := req.Params["requestType"].(string); ok {
|
||||||
|
event.RequestType = requestType
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("AppPicker: Broadcasting event: %+v", event)
|
||||||
|
manager.RequestOpen(event)
|
||||||
|
models.Respond(conn, req.ID, "ok")
|
||||||
|
log.Infof("AppPicker: Request handled successfully")
|
||||||
|
}
|
||||||
48
core/internal/server/apppicker/manager.go
Normal file
48
core/internal/server/apppicker/manager.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package apppicker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
subscribers syncmap.Map[string, chan OpenEvent]
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Subscribe(id string) chan OpenEvent {
|
||||||
|
ch := make(chan OpenEvent, 16)
|
||||||
|
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) RequestOpen(event OpenEvent) {
|
||||||
|
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
|
||||||
|
select {
|
||||||
|
case ch <- event:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() {
|
||||||
|
m.closeOnce.Do(func() {
|
||||||
|
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
|
||||||
|
close(ch)
|
||||||
|
m.subscribers.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
8
core/internal/server/apppicker/models.go
Normal file
8
core/internal/server/apppicker/models.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package apppicker
|
||||||
|
|
||||||
|
type OpenEvent struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
MimeType string `json:"mimeType,omitempty"`
|
||||||
|
Categories []string `json:"categories,omitempty"`
|
||||||
|
RequestType string `json:"requestType"`
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResult struct {
|
type SuccessResult struct {
|
||||||
@@ -190,7 +190,7 @@ func handlePairingSubmit(conn net.Conn, req Request, manager *Manager) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsRaw, ok := req.Params["secrets"].(map[string]interface{})
|
secretsRaw, ok := req.Params["secrets"].(map[string]any)
|
||||||
secrets := make(map[string]string)
|
secrets := make(map[string]string)
|
||||||
if ok {
|
if ok {
|
||||||
for k, v := range secretsRaw {
|
for k, v := range secretsRaw {
|
||||||
|
|||||||
@@ -37,25 +37,22 @@ func NewDDCBackend() (*DDCBackend, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *DDCBackend) scanI2CDevices() error {
|
func (b *DDCBackend) scanI2CDevices() error {
|
||||||
b.scanMutex.Lock()
|
return b.scanI2CDevicesInternal(false)
|
||||||
lastScan := b.lastScan
|
}
|
||||||
b.scanMutex.Unlock()
|
|
||||||
|
|
||||||
if time.Since(lastScan) < b.scanInterval {
|
func (b *DDCBackend) ForceRescan() error {
|
||||||
return nil
|
return b.scanI2CDevicesInternal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
||||||
b.scanMutex.Lock()
|
b.scanMutex.Lock()
|
||||||
defer b.scanMutex.Unlock()
|
defer b.scanMutex.Unlock()
|
||||||
|
|
||||||
if time.Since(b.lastScan) < b.scanInterval {
|
if !force && time.Since(b.lastScan) < b.scanInterval {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
b.devices.Range(func(key string, value *ddcDevice) bool {
|
activeBuses := make(map[int]bool)
|
||||||
b.devices.Delete(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
for i := 0; i < 32; i++ {
|
for i := 0; i < 32; i++ {
|
||||||
busPath := fmt.Sprintf("/dev/i2c-%d", i)
|
busPath := fmt.Sprintf("/dev/i2c-%d", i)
|
||||||
@@ -68,17 +65,31 @@ func (b *DDCBackend) scanI2CDevices() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeBuses[i] = true
|
||||||
|
id := fmt.Sprintf("ddc:i2c-%d", i)
|
||||||
|
|
||||||
|
if _, exists := b.devices.Load(id); exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
dev, err := b.probeDDCDevice(i)
|
dev, err := b.probeDDCDevice(i)
|
||||||
if err != nil || dev == nil {
|
if err != nil || dev == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
id := fmt.Sprintf("ddc:i2c-%d", i)
|
|
||||||
dev.id = id
|
dev.id = id
|
||||||
b.devices.Store(id, dev)
|
b.devices.Store(id, dev)
|
||||||
log.Debugf("found DDC device on i2c-%d", i)
|
log.Debugf("found DDC device on i2c-%d", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.devices.Range(func(id string, dev *ddcDevice) bool {
|
||||||
|
if !activeBuses[dev.bus] {
|
||||||
|
b.devices.Delete(id)
|
||||||
|
log.Debugf("removed DDC device %s (bus no longer exists)", id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
b.lastScan = time.Now()
|
b.lastScan = time.Now()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -187,6 +198,13 @@ func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callb
|
|||||||
func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error {
|
func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error {
|
||||||
_, ok := b.devices.Load(id)
|
_, ok := b.devices.Load(id)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
if err := b.scanI2CDevicesInternal(true); err != nil {
|
||||||
|
log.Debugf("rescan failed for %s: %v", id, err)
|
||||||
|
}
|
||||||
|
_, ok = b.devices.Load(id)
|
||||||
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("device not found: %s", id)
|
return fmt.Errorf("device not found: %s", id)
|
||||||
}
|
}
|
||||||
@@ -234,14 +252,29 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error {
|
func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) error {
|
||||||
dev, ok := b.devices.Load(id)
|
dev, ok := b.devices.Load(id)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
if err := b.scanI2CDevicesInternal(true); err != nil {
|
||||||
|
log.Debugf("rescan failed for %s: %v", id, err)
|
||||||
|
}
|
||||||
|
dev, ok = b.devices.Load(id)
|
||||||
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("device not found: %s", id)
|
return fmt.Errorf("device not found: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
||||||
|
|
||||||
|
if _, err := os.Stat(busPath); os.IsNotExist(err) {
|
||||||
|
b.devices.Delete(id)
|
||||||
|
log.Debugf("removed stale DDC device %s (bus no longer exists)", id)
|
||||||
|
return fmt.Errorf("device disconnected: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
b.devices.Delete(id)
|
||||||
|
log.Debugf("removed DDC device %s (open failed: %v)", id, err)
|
||||||
return fmt.Errorf("open i2c device: %w", err)
|
return fmt.Errorf("open i2c device: %w", err)
|
||||||
}
|
}
|
||||||
defer syscall.Close(fd)
|
defer syscall.Close(fd)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func (m *Manager) initSysfs() {
|
|||||||
m.sysfsBackend = sysfs
|
m.sysfsBackend = sysfs
|
||||||
m.sysfsReady = true
|
m.sysfsReady = true
|
||||||
m.updateState()
|
m.updateState()
|
||||||
|
m.initUdev()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +66,11 @@ func (m *Manager) initSysfs() {
|
|||||||
m.sysfsBackend = sysfs
|
m.sysfsBackend = sysfs
|
||||||
m.sysfsReady = true
|
m.sysfsReady = true
|
||||||
m.updateState()
|
m.updateState()
|
||||||
|
m.initUdev()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) initUdev() {
|
||||||
|
m.udevMonitor = NewUdevMonitor(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) initDDC() {
|
func (m *Manager) initDDC() {
|
||||||
@@ -83,6 +89,13 @@ func (m *Manager) initDDC() {
|
|||||||
|
|
||||||
func (m *Manager) Rescan() {
|
func (m *Manager) Rescan() {
|
||||||
log.Debug("Rescanning brightness devices...")
|
log.Debug("Rescanning brightness devices...")
|
||||||
|
|
||||||
|
if m.ddcReady && m.ddcBackend != nil {
|
||||||
|
if err := m.ddcBackend.ForceRescan(); err != nil {
|
||||||
|
log.Debugf("DDC force rescan failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,15 +34,16 @@ type DeviceUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID interface{} `json:"id"`
|
ID any `json:"id"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]any `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
logindBackend *LogindBackend
|
logindBackend *LogindBackend
|
||||||
sysfsBackend *SysfsBackend
|
sysfsBackend *SysfsBackend
|
||||||
ddcBackend *DDCBackend
|
ddcBackend *DDCBackend
|
||||||
|
udevMonitor *UdevMonitor
|
||||||
|
|
||||||
logindReady bool
|
logindReady bool
|
||||||
sysfsReady bool
|
sysfsReady bool
|
||||||
@@ -181,6 +182,10 @@ func (m *Manager) Close() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if m.udevMonitor != nil {
|
||||||
|
m.udevMonitor.Close()
|
||||||
|
}
|
||||||
|
|
||||||
if m.logindBackend != nil {
|
if m.logindBackend != nil {
|
||||||
m.logindBackend.Close()
|
m.logindBackend.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
197
core/internal/server/brightness/udev.go
Normal file
197
core/internal/server/brightness/udev.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package brightness
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/pilebones/go-udev/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UdevMonitor struct {
|
||||||
|
stop chan struct{}
|
||||||
|
rescanMutex sync.Mutex
|
||||||
|
rescanTimer *time.Timer
|
||||||
|
rescanPending bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
||||||
|
m := &UdevMonitor{
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
go m.run(manager)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) run(manager *Manager) {
|
||||||
|
conn := &netlink.UEventConn{}
|
||||||
|
if err := conn.Connect(netlink.UdevEvent); err != nil {
|
||||||
|
log.Errorf("Failed to connect to udev netlink: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
matcher := &netlink.RuleDefinitions{
|
||||||
|
Rules: []netlink.RuleDefinition{
|
||||||
|
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
||||||
|
{Env: map[string]string{"SUBSYSTEM": "drm"}},
|
||||||
|
{Env: map[string]string{"SUBSYSTEM": "i2c"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := matcher.Compile(); err != nil {
|
||||||
|
log.Errorf("Failed to compile udev matcher: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
events := make(chan netlink.UEvent)
|
||||||
|
errs := make(chan error)
|
||||||
|
conn.Monitor(events, errs, matcher)
|
||||||
|
|
||||||
|
log.Info("Udev monitor started for backlight/drm/i2c events")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.stop:
|
||||||
|
return
|
||||||
|
case err := <-errs:
|
||||||
|
log.Errorf("Udev monitor error: %v", err)
|
||||||
|
return
|
||||||
|
case event := <-events:
|
||||||
|
m.handleEvent(manager, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) handleEvent(manager *Manager, event netlink.UEvent) {
|
||||||
|
subsystem := event.Env["SUBSYSTEM"]
|
||||||
|
devpath := event.Env["DEVPATH"]
|
||||||
|
|
||||||
|
if subsystem == "" || devpath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sysname := filepath.Base(devpath)
|
||||||
|
action := string(event.Action)
|
||||||
|
|
||||||
|
switch subsystem {
|
||||||
|
case "drm", "i2c":
|
||||||
|
m.handleDisplayEvent(manager, action, subsystem, sysname)
|
||||||
|
case "backlight":
|
||||||
|
m.handleBacklightEvent(manager, action, sysname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) handleDisplayEvent(manager *Manager, action, subsystem, sysname string) {
|
||||||
|
switch action {
|
||||||
|
case "add", "remove", "change":
|
||||||
|
log.Debugf("Udev %s event: %s:%s - queueing DDC rescan", action, subsystem, sysname)
|
||||||
|
m.debouncedRescan(manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) debouncedRescan(manager *Manager) {
|
||||||
|
m.rescanMutex.Lock()
|
||||||
|
defer m.rescanMutex.Unlock()
|
||||||
|
|
||||||
|
m.rescanPending = true
|
||||||
|
|
||||||
|
if m.rescanTimer != nil {
|
||||||
|
m.rescanTimer.Reset(2 * time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
|
||||||
|
m.rescanMutex.Lock()
|
||||||
|
pending := m.rescanPending
|
||||||
|
m.rescanPending = false
|
||||||
|
m.rescanMutex.Unlock()
|
||||||
|
|
||||||
|
if !pending {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Executing debounced DDC rescan")
|
||||||
|
manager.Rescan()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) handleBacklightEvent(manager *Manager, action, sysname string) {
|
||||||
|
switch action {
|
||||||
|
case "change":
|
||||||
|
m.handleChange(manager, "backlight", sysname)
|
||||||
|
case "add", "remove":
|
||||||
|
log.Debugf("Udev %s event: backlight:%s - triggering rescan", action, sysname)
|
||||||
|
manager.Rescan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) handleChange(manager *Manager, subsystem, sysname string) {
|
||||||
|
deviceID := subsystem + ":" + sysname
|
||||||
|
|
||||||
|
if manager.sysfsBackend == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
brightnessPath := filepath.Join(manager.sysfsBackend.basePath, subsystem, sysname, "brightness")
|
||||||
|
data, err := os.ReadFile(brightnessPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Udev change event for %s but failed to read brightness: %v", deviceID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
brightness, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to parse brightness for %s: %v", deviceID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.handleUdevBrightnessChange(deviceID, brightness)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UdevMonitor) Close() {
|
||||||
|
close(m.stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleUdevBrightnessChange(deviceID string, rawBrightness int) {
|
||||||
|
if m.sysfsBackend == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dev, err := m.sysfsBackend.GetDevice(deviceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Udev event for unknown device %s: %v", deviceID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
percent := m.sysfsBackend.ValueToPercent(rawBrightness, dev, false)
|
||||||
|
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
var found bool
|
||||||
|
for i, d := range m.state.Devices {
|
||||||
|
if d.ID != deviceID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
if d.Current == rawBrightness {
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.state.Devices[i].Current = rawBrightness
|
||||||
|
m.state.Devices[i].CurrentPercent = percent
|
||||||
|
break
|
||||||
|
}
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
log.Debugf("Udev event for device not in state: %s", deviceID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Udev brightness change: %s -> %d (%d%%)", deviceID, rawBrightness, percent)
|
||||||
|
m.broadcastDeviceUpdate(deviceID)
|
||||||
|
}
|
||||||
260
core/internal/server/brightness/udev_test.go
Normal file
260
core/internal/server/brightness/udev_test.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
package brightness
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pilebones/go-udev/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestManager(t *testing.T) (*Manager, string) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
backlightDir := filepath.Join(tmpDir, "backlight", "intel_backlight")
|
||||||
|
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("1000\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("500\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sysfs := &SysfsBackend{
|
||||||
|
basePath: tmpDir,
|
||||||
|
classes: []string{"backlight"},
|
||||||
|
}
|
||||||
|
if err := sysfs.scanDevices(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &Manager{
|
||||||
|
sysfsBackend: sysfs,
|
||||||
|
sysfsReady: true,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.state = State{
|
||||||
|
Devices: []Device{
|
||||||
|
{
|
||||||
|
Class: ClassBacklight,
|
||||||
|
ID: "backlight:intel_backlight",
|
||||||
|
Name: "intel_backlight",
|
||||||
|
Current: 500,
|
||||||
|
Max: 1000,
|
||||||
|
CurrentPercent: 50,
|
||||||
|
Backend: "sysfs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tmpDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUdevBrightnessChange_UpdatesState(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
|
||||||
|
m.handleUdevBrightnessChange("backlight:intel_backlight", 750)
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
if len(state.Devices) != 1 {
|
||||||
|
t.Fatalf("expected 1 device, got %d", len(state.Devices))
|
||||||
|
}
|
||||||
|
|
||||||
|
dev := state.Devices[0]
|
||||||
|
if dev.Current != 750 {
|
||||||
|
t.Errorf("expected Current=750, got %d", dev.Current)
|
||||||
|
}
|
||||||
|
if dev.CurrentPercent != 75 {
|
||||||
|
t.Errorf("expected CurrentPercent=75, got %d", dev.CurrentPercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUdevBrightnessChange_NoChangeWhenSameValue(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
|
||||||
|
updateCh := m.SubscribeUpdates("test")
|
||||||
|
defer m.UnsubscribeUpdates("test")
|
||||||
|
|
||||||
|
m.handleUdevBrightnessChange("backlight:intel_backlight", 500)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-updateCh:
|
||||||
|
t.Error("should not broadcast when brightness unchanged")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUdevBrightnessChange_BroadcastsOnChange(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
|
||||||
|
updateCh := m.SubscribeUpdates("test")
|
||||||
|
defer m.UnsubscribeUpdates("test")
|
||||||
|
|
||||||
|
m.handleUdevBrightnessChange("backlight:intel_backlight", 750)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case update := <-updateCh:
|
||||||
|
if update.Device.Current != 750 {
|
||||||
|
t.Errorf("broadcast had wrong Current: got %d, want 750", update.Device.Current)
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Error("expected broadcast on brightness change")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUdevBrightnessChange_UnknownDevice(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
|
||||||
|
m.handleUdevBrightnessChange("backlight:unknown_device", 500)
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
if len(state.Devices) != 1 {
|
||||||
|
t.Errorf("state should be unchanged, got %d devices", len(state.Devices))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUdevBrightnessChange_NilSysfsBackend(t *testing.T) {
|
||||||
|
m := &Manager{
|
||||||
|
sysfsBackend: nil,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.handleUdevBrightnessChange("backlight:test", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUdevBrightnessChange_DeviceNotInState(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
|
||||||
|
m.sysfsBackend.deviceCache.Store("backlight:other_device", &sysfsDevice{
|
||||||
|
class: ClassBacklight,
|
||||||
|
id: "backlight:other_device",
|
||||||
|
name: "other_device",
|
||||||
|
maxBrightness: 100,
|
||||||
|
minValue: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
m.handleUdevBrightnessChange("backlight:other_device", 50)
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
for _, d := range state.Devices {
|
||||||
|
if d.ID == "backlight:other_device" {
|
||||||
|
t.Error("device should not be added to state via udev change event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleEvent_ChangeAction(t *testing.T) {
|
||||||
|
m, tmpDir := setupTestManager(t)
|
||||||
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
|
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
||||||
|
if err := os.WriteFile(brightnessPath, []byte("800\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
event := netlink.UEvent{
|
||||||
|
Action: netlink.CHANGE,
|
||||||
|
Env: map[string]string{
|
||||||
|
"SUBSYSTEM": "backlight",
|
||||||
|
"DEVPATH": "/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
um.handleEvent(m, event)
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
if state.Devices[0].Current != 800 {
|
||||||
|
t.Errorf("expected Current=800 after change event, got %d", state.Devices[0].Current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleEvent_MissingEnvVars(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
|
event := netlink.UEvent{
|
||||||
|
Action: netlink.CHANGE,
|
||||||
|
Env: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
um.handleEvent(m, event)
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
if state.Devices[0].Current != 500 {
|
||||||
|
t.Error("state should be unchanged with missing env vars")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleEvent_MissingSubsystem(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
|
event := netlink.UEvent{
|
||||||
|
Action: netlink.CHANGE,
|
||||||
|
Env: map[string]string{
|
||||||
|
"DEVPATH": "/devices/foo/bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
um.handleEvent(m, event)
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
if state.Devices[0].Current != 500 {
|
||||||
|
t.Error("state should be unchanged with missing SUBSYSTEM")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleChange_BrightnessFileNotFound(t *testing.T) {
|
||||||
|
m, _ := setupTestManager(t)
|
||||||
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
|
um.handleChange(m, "backlight", "nonexistent_device")
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
if state.Devices[0].Current != 500 {
|
||||||
|
t.Error("state should be unchanged when brightness file not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleChange_InvalidBrightnessValue(t *testing.T) {
|
||||||
|
m, tmpDir := setupTestManager(t)
|
||||||
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
|
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
||||||
|
if err := os.WriteFile(brightnessPath, []byte("not_a_number\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
um.handleChange(m, "backlight", "intel_backlight")
|
||||||
|
|
||||||
|
state := m.GetState()
|
||||||
|
if state.Devices[0].Current != 500 {
|
||||||
|
t.Error("state should be unchanged with invalid brightness value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUdevMonitor_Close(t *testing.T) {
|
||||||
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
|
um.Close()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-um.stop:
|
||||||
|
default:
|
||||||
|
t.Error("stop channel should be closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleChange_NilSysfsBackend(t *testing.T) {
|
||||||
|
m := &Manager{
|
||||||
|
sysfsBackend: nil,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
um := &UdevMonitor{stop: make(chan struct{})}
|
||||||
|
|
||||||
|
um.handleChange(m, "backlight", "test_device")
|
||||||
|
}
|
||||||
28
core/internal/server/browser/handlers.go
Normal file
28
core/internal/server/browser/handlers.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params map[string]any `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
|
||||||
|
switch req.Method {
|
||||||
|
case "browser.open":
|
||||||
|
url, ok := req.Params["url"].(string)
|
||||||
|
if !ok {
|
||||||
|
models.RespondError(conn, req.ID, "invalid url parameter")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
manager.RequestOpen(url)
|
||||||
|
models.Respond(conn, req.ID, "ok")
|
||||||
|
default:
|
||||||
|
models.RespondError(conn, req.ID, "unknown method")
|
||||||
|
}
|
||||||
|
}
|
||||||
49
core/internal/server/browser/manager.go
Normal file
49
core/internal/server/browser/manager.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package browser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
subscribers syncmap.Map[string, chan OpenEvent]
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Subscribe(id string) chan OpenEvent {
|
||||||
|
ch := make(chan OpenEvent, 16)
|
||||||
|
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) RequestOpen(url string) {
|
||||||
|
event := OpenEvent{URL: url}
|
||||||
|
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
|
||||||
|
select {
|
||||||
|
case ch <- event:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() {
|
||||||
|
m.closeOnce.Do(func() {
|
||||||
|
m.subscribers.Range(func(key string, ch chan OpenEvent) bool {
|
||||||
|
close(ch)
|
||||||
|
m.subscribers.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
5
core/internal/server/browser/models.go
Normal file
5
core/internal/server/browser/models.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package browser
|
||||||
|
|
||||||
|
type OpenEvent struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResult struct {
|
type SuccessResult struct {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func TestHandleGetPrinters_Error(t *testing.T) {
|
|||||||
|
|
||||||
handleGetPrinters(conn, req, m)
|
handleGetPrinters(conn, req, m)
|
||||||
|
|
||||||
var resp models.Response[interface{}]
|
var resp models.Response[any]
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
err := json.NewDecoder(buf).Decode(&resp)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Nil(t, resp.Result)
|
assert.Nil(t, resp.Result)
|
||||||
@@ -103,7 +103,7 @@ func TestHandleGetJobs(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.getJobs",
|
Method: "cups.getJobs",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"printerName": "printer1",
|
"printerName": "printer1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -130,12 +130,12 @@ func TestHandleGetJobs_MissingParam(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.getJobs",
|
Method: "cups.getJobs",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGetJobs(conn, req, m)
|
handleGetJobs(conn, req, m)
|
||||||
|
|
||||||
var resp models.Response[interface{}]
|
var resp models.Response[any]
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
err := json.NewDecoder(buf).Decode(&resp)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Nil(t, resp.Result)
|
assert.Nil(t, resp.Result)
|
||||||
@@ -155,7 +155,7 @@ func TestHandlePausePrinter(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.pausePrinter",
|
Method: "cups.pausePrinter",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"printerName": "printer1",
|
"printerName": "printer1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ func TestHandleResumePrinter(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.resumePrinter",
|
Method: "cups.resumePrinter",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"printerName": "printer1",
|
"printerName": "printer1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ func TestHandleCancelJob(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.cancelJob",
|
Method: "cups.cancelJob",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"jobID": float64(1),
|
"jobID": float64(1),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ func TestHandlePurgeJobs(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.purgeJobs",
|
Method: "cups.purgeJobs",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"printerName": "printer1",
|
"printerName": "printer1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ func TestHandleRequest_UnknownMethod(t *testing.T) {
|
|||||||
|
|
||||||
HandleRequest(conn, req, m)
|
HandleRequest(conn, req, m)
|
||||||
|
|
||||||
var resp models.Response[interface{}]
|
var resp models.Response[any]
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
err := json.NewDecoder(buf).Decode(&resp)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Nil(t, resp.Result)
|
assert.Nil(t, resp.Result)
|
||||||
@@ -356,7 +356,7 @@ func TestHandleCreatePrinter(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.createPrinter",
|
Method: "cups.createPrinter",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"name": "newprinter",
|
"name": "newprinter",
|
||||||
"deviceURI": "usb://HP",
|
"deviceURI": "usb://HP",
|
||||||
"ppd": "generic.ppd",
|
"ppd": "generic.ppd",
|
||||||
@@ -377,10 +377,10 @@ func TestHandleCreatePrinter_MissingParams(t *testing.T) {
|
|||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
conn := &mockConn{Buffer: buf}
|
conn := &mockConn{Buffer: buf}
|
||||||
|
|
||||||
req := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]interface{}{}}
|
req := Request{ID: 1, Method: "cups.createPrinter", Params: map[string]any{}}
|
||||||
handleCreatePrinter(conn, req, m)
|
handleCreatePrinter(conn, req, m)
|
||||||
|
|
||||||
var resp models.Response[interface{}]
|
var resp models.Response[any]
|
||||||
err := json.NewDecoder(buf).Decode(&resp)
|
err := json.NewDecoder(buf).Decode(&resp)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Nil(t, resp.Result)
|
assert.Nil(t, resp.Result)
|
||||||
@@ -399,7 +399,7 @@ func TestHandleDeletePrinter(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.deletePrinter",
|
Method: "cups.deletePrinter",
|
||||||
Params: map[string]interface{}{"printerName": "printer1"},
|
Params: map[string]any{"printerName": "printer1"},
|
||||||
}
|
}
|
||||||
handleDeletePrinter(conn, req, m)
|
handleDeletePrinter(conn, req, m)
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ func TestHandleAcceptJobs(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.acceptJobs",
|
Method: "cups.acceptJobs",
|
||||||
Params: map[string]interface{}{"printerName": "printer1"},
|
Params: map[string]any{"printerName": "printer1"},
|
||||||
}
|
}
|
||||||
handleAcceptJobs(conn, req, m)
|
handleAcceptJobs(conn, req, m)
|
||||||
|
|
||||||
@@ -445,7 +445,7 @@ func TestHandleRejectJobs(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.rejectJobs",
|
Method: "cups.rejectJobs",
|
||||||
Params: map[string]interface{}{"printerName": "printer1"},
|
Params: map[string]any{"printerName": "printer1"},
|
||||||
}
|
}
|
||||||
handleRejectJobs(conn, req, m)
|
handleRejectJobs(conn, req, m)
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ func TestHandleSetPrinterShared(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.setPrinterShared",
|
Method: "cups.setPrinterShared",
|
||||||
Params: map[string]interface{}{"printerName": "printer1", "shared": true},
|
Params: map[string]any{"printerName": "printer1", "shared": true},
|
||||||
}
|
}
|
||||||
handleSetPrinterShared(conn, req, m)
|
handleSetPrinterShared(conn, req, m)
|
||||||
|
|
||||||
@@ -491,7 +491,7 @@ func TestHandleSetPrinterLocation(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.setPrinterLocation",
|
Method: "cups.setPrinterLocation",
|
||||||
Params: map[string]interface{}{"printerName": "printer1", "location": "Office"},
|
Params: map[string]any{"printerName": "printer1", "location": "Office"},
|
||||||
}
|
}
|
||||||
handleSetPrinterLocation(conn, req, m)
|
handleSetPrinterLocation(conn, req, m)
|
||||||
|
|
||||||
@@ -514,7 +514,7 @@ func TestHandleSetPrinterInfo(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.setPrinterInfo",
|
Method: "cups.setPrinterInfo",
|
||||||
Params: map[string]interface{}{"printerName": "printer1", "info": "Main Printer"},
|
Params: map[string]any{"printerName": "printer1", "info": "Main Printer"},
|
||||||
}
|
}
|
||||||
handleSetPrinterInfo(conn, req, m)
|
handleSetPrinterInfo(conn, req, m)
|
||||||
|
|
||||||
@@ -537,7 +537,7 @@ func TestHandleMoveJob(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.moveJob",
|
Method: "cups.moveJob",
|
||||||
Params: map[string]interface{}{"jobID": float64(1), "destPrinter": "printer2"},
|
Params: map[string]any{"jobID": float64(1), "destPrinter": "printer2"},
|
||||||
}
|
}
|
||||||
handleMoveJob(conn, req, m)
|
handleMoveJob(conn, req, m)
|
||||||
|
|
||||||
@@ -560,7 +560,7 @@ func TestHandlePrintTestPage(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.printTestPage",
|
Method: "cups.printTestPage",
|
||||||
Params: map[string]interface{}{"printerName": "printer1"},
|
Params: map[string]any{"printerName": "printer1"},
|
||||||
}
|
}
|
||||||
handlePrintTestPage(conn, req, m)
|
handlePrintTestPage(conn, req, m)
|
||||||
|
|
||||||
@@ -584,7 +584,7 @@ func TestHandleAddPrinterToClass(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.addPrinterToClass",
|
Method: "cups.addPrinterToClass",
|
||||||
Params: map[string]interface{}{"className": "office", "printerName": "printer1"},
|
Params: map[string]any{"className": "office", "printerName": "printer1"},
|
||||||
}
|
}
|
||||||
handleAddPrinterToClass(conn, req, m)
|
handleAddPrinterToClass(conn, req, m)
|
||||||
|
|
||||||
@@ -607,7 +607,7 @@ func TestHandleRemovePrinterFromClass(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.removePrinterFromClass",
|
Method: "cups.removePrinterFromClass",
|
||||||
Params: map[string]interface{}{"className": "office", "printerName": "printer1"},
|
Params: map[string]any{"className": "office", "printerName": "printer1"},
|
||||||
}
|
}
|
||||||
handleRemovePrinterFromClass(conn, req, m)
|
handleRemovePrinterFromClass(conn, req, m)
|
||||||
|
|
||||||
@@ -630,7 +630,7 @@ func TestHandleDeleteClass(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.deleteClass",
|
Method: "cups.deleteClass",
|
||||||
Params: map[string]interface{}{"className": "office"},
|
Params: map[string]any{"className": "office"},
|
||||||
}
|
}
|
||||||
handleDeleteClass(conn, req, m)
|
handleDeleteClass(conn, req, m)
|
||||||
|
|
||||||
@@ -653,7 +653,7 @@ func TestHandleRestartJob(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.restartJob",
|
Method: "cups.restartJob",
|
||||||
Params: map[string]interface{}{"jobID": float64(1)},
|
Params: map[string]any{"jobID": float64(1)},
|
||||||
}
|
}
|
||||||
handleRestartJob(conn, req, m)
|
handleRestartJob(conn, req, m)
|
||||||
|
|
||||||
@@ -676,7 +676,7 @@ func TestHandleHoldJob(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.holdJob",
|
Method: "cups.holdJob",
|
||||||
Params: map[string]interface{}{"jobID": float64(1)},
|
Params: map[string]any{"jobID": float64(1)},
|
||||||
}
|
}
|
||||||
handleHoldJob(conn, req, m)
|
handleHoldJob(conn, req, m)
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ func TestHandleHoldJob_WithHoldUntil(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "cups.holdJob",
|
Method: "cups.holdJob",
|
||||||
Params: map[string]interface{}{"jobID": float64(1), "holdUntil": "no-hold"},
|
Params: map[string]any{"jobID": float64(1), "holdUntil": "no-hold"},
|
||||||
}
|
}
|
||||||
handleHoldJob(conn, req, m)
|
handleHoldJob(conn, req, m)
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func (p *DBusPkHelper) JobSetHoldUntil(jobID int, holdUntil string) error {
|
|||||||
return p.callSimple("JobSetHoldUntil", int32(jobID), holdUntil)
|
return p.callSimple("JobSetHoldUntil", int32(jobID), holdUntil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DBusPkHelper) callSimple(method string, args ...interface{}) error {
|
func (p *DBusPkHelper) callSimple(method string, args ...any) error {
|
||||||
var errStr string
|
var errStr string
|
||||||
|
|
||||||
call := p.obj.Call(pkHelperInterface+"."+method, 0, args...)
|
call := p.obj.Call(pkHelperInterface+"."+method, 0, args...)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (sm *SubscriptionManager) createSubscription() (int, error) {
|
|||||||
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
|
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
|
||||||
|
|
||||||
// Subscription attributes go in SubscriptionAttributes (subscription-attributes-tag in IPP)
|
// Subscription attributes go in SubscriptionAttributes (subscription-attributes-tag in IPP)
|
||||||
req.SubscriptionAttributes = map[string]interface{}{
|
req.SubscriptionAttributes = map[string]any{
|
||||||
"notify-events": []string{
|
"notify-events": []string{
|
||||||
"printer-state-changed",
|
"printer-state-changed",
|
||||||
"printer-added",
|
"printer-added",
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (sm *DBusSubscriptionManager) createDBusSubscription() (int, error) {
|
|||||||
req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL)
|
req.OperationAttributes[ipp.AttributePrinterURI] = fmt.Sprintf("%s/", sm.baseURL)
|
||||||
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
|
req.OperationAttributes[ipp.AttributeRequestingUserName] = "dms"
|
||||||
|
|
||||||
req.SubscriptionAttributes = map[string]interface{}{
|
req.SubscriptionAttributes = map[string]any{
|
||||||
"notify-events": []string{
|
"notify-events": []string{
|
||||||
"printer-state-changed",
|
"printer-state-changed",
|
||||||
"printer-added",
|
"printer-added",
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResult struct {
|
type SuccessResult struct {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type Manager struct {
|
|||||||
display *wlclient.Display
|
display *wlclient.Display
|
||||||
ctx *wlclient.Context
|
ctx *wlclient.Context
|
||||||
registry *wlclient.Registry
|
registry *wlclient.Registry
|
||||||
manager interface{}
|
manager any
|
||||||
|
|
||||||
outputs syncmap.Map[uint32, *outputState]
|
outputs syncmap.Map[uint32, *outputState]
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ type outputState struct {
|
|||||||
id uint32
|
id uint32
|
||||||
registryName uint32
|
registryName uint32
|
||||||
output *wlclient.Output
|
output *wlclient.Output
|
||||||
ipcOutput interface{}
|
ipcOutput any
|
||||||
name string
|
name string
|
||||||
active uint32
|
active uint32
|
||||||
tags []TagState
|
tags []TagState
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID interface{} `json:"id"`
|
ID any `json:"id"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]any `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleRequest(conn net.Conn, req Request, m *Manager) {
|
func HandleRequest(conn net.Conn, req Request, m *Manager) {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func TestHandleRequest(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "evdev.getState",
|
Method: "evdev.getState",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleRequest(conn, req, m)
|
HandleRequest(conn, req, m)
|
||||||
@@ -85,7 +85,7 @@ func TestHandleRequest(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 456,
|
ID: 456,
|
||||||
Method: "evdev.unknownMethod",
|
Method: "evdev.unknownMethod",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleRequest(conn, req, m)
|
HandleRequest(conn, req, m)
|
||||||
@@ -114,7 +114,7 @@ func TestHandleGetState(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 789,
|
ID: 789,
|
||||||
Method: "evdev.getState",
|
Method: "evdev.getState",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGetState(conn, req, m)
|
handleGetState(conn, req, m)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResult struct {
|
type SuccessResult struct {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResult struct {
|
type SuccessResult struct {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func mockGetAllAccountsProperties() *dbus.Call {
|
|||||||
"Locked": dbus.MakeVariant(false),
|
"Locked": dbus.MakeVariant(false),
|
||||||
"PasswordMode": dbus.MakeVariant(int32(1)),
|
"PasswordMode": dbus.MakeVariant(int32(1)),
|
||||||
}
|
}
|
||||||
return &dbus.Call{Err: nil, Body: []interface{}{props}}
|
return &dbus.Call{Err: nil, Body: []any{props}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRespondError_Freedesktop(t *testing.T) {
|
func TestRespondError_Freedesktop(t *testing.T) {
|
||||||
@@ -134,7 +134,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setIconFile",
|
Method: "freedesktop.accounts.setIconFile",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetIconFile(conn, req, manager)
|
handleSetIconFile(conn, req, manager)
|
||||||
@@ -167,7 +167,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setIconFile",
|
Method: "freedesktop.accounts.setIconFile",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"path": "/path/to/icon.png",
|
"path": "/path/to/icon.png",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -199,7 +199,7 @@ func TestHandleSetIconFile(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setIconFile",
|
Method: "freedesktop.accounts.setIconFile",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"path": "/path/to/icon.png",
|
"path": "/path/to/icon.png",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ func TestHandleSetRealName(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setRealName",
|
Method: "freedesktop.accounts.setRealName",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetRealName(conn, req, manager)
|
handleSetRealName(conn, req, manager)
|
||||||
@@ -259,7 +259,7 @@ func TestHandleSetRealName(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setRealName",
|
Method: "freedesktop.accounts.setRealName",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"name": "New Name",
|
"name": "New Name",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ func TestHandleSetEmail(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setEmail",
|
Method: "freedesktop.accounts.setEmail",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetEmail(conn, req, manager)
|
handleSetEmail(conn, req, manager)
|
||||||
@@ -322,7 +322,7 @@ func TestHandleSetEmail(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setEmail",
|
Method: "freedesktop.accounts.setEmail",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"email": "test@example.com",
|
"email": "test@example.com",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -352,7 +352,7 @@ func TestHandleSetLanguage(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setLanguage",
|
Method: "freedesktop.accounts.setLanguage",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetLanguage(conn, req, manager)
|
handleSetLanguage(conn, req, manager)
|
||||||
@@ -377,7 +377,7 @@ func TestHandleSetLocation(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.setLocation",
|
Method: "freedesktop.accounts.setLocation",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetLocation(conn, req, manager)
|
handleSetLocation(conn, req, manager)
|
||||||
@@ -402,7 +402,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.getUserIconFile",
|
Method: "freedesktop.accounts.getUserIconFile",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleGetUserIconFile(conn, req, manager)
|
handleGetUserIconFile(conn, req, manager)
|
||||||
@@ -429,7 +429,7 @@ func TestHandleGetUserIconFile(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "freedesktop.accounts.getUserIconFile",
|
Method: "freedesktop.accounts.getUserIconFile",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"username": "testuser",
|
"username": "testuser",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -473,7 +473,7 @@ func TestHandleGetColorScheme(t *testing.T) {
|
|||||||
mockSettingsObj := mockdbus.NewMockBusObject(t)
|
mockSettingsObj := mockdbus.NewMockBusObject(t)
|
||||||
mockCall := &dbus.Call{
|
mockCall := &dbus.Call{
|
||||||
Err: nil,
|
Err: nil,
|
||||||
Body: []interface{}{dbus.MakeVariant(uint32(1))},
|
Body: []any{dbus.MakeVariant(uint32(1))},
|
||||||
}
|
}
|
||||||
mockSettingsObj.EXPECT().Call("org.freedesktop.portal.Settings.ReadOne", dbus.Flags(0), "org.freedesktop.appearance", "color-scheme").Return(mockCall)
|
mockSettingsObj.EXPECT().Call("org.freedesktop.portal.Settings.ReadOne", dbus.Flags(0), "org.freedesktop.appearance", "color-scheme").Return(mockCall)
|
||||||
|
|
||||||
@@ -564,7 +564,7 @@ func TestHandleRequest(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: method,
|
Method: method,
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleRequest(conn, req, manager)
|
HandleRequest(conn, req, manager)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
Params map[string]any `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessResult struct {
|
type SuccessResult struct {
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ func TestHandleSetIdleHint(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "loginctl.setIdleHint",
|
Method: "loginctl.setIdleHint",
|
||||||
Params: map[string]interface{}{},
|
Params: map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetIdleHint(conn, req, manager)
|
handleSetIdleHint(conn, req, manager)
|
||||||
@@ -294,7 +294,7 @@ func TestHandleSetIdleHint(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "loginctl.setIdleHint",
|
Method: "loginctl.setIdleHint",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"idle": true,
|
"idle": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -327,7 +327,7 @@ func TestHandleSetIdleHint(t *testing.T) {
|
|||||||
req := Request{
|
req := Request{
|
||||||
ID: 123,
|
ID: 123,
|
||||||
Method: "loginctl.setIdleHint",
|
Method: "loginctl.setIdleHint",
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"idle": false,
|
"idle": false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func (m *Manager) updateSessionState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v, ok := props["User"]; ok {
|
if v, ok := props["User"]; ok {
|
||||||
if userArr, ok := v.Value().([]interface{}); ok && len(userArr) >= 1 {
|
if userArr, ok := v.Value().([]any); ok && len(userArr) >= 1 {
|
||||||
if uid, ok := userArr[0].(uint32); ok {
|
if uid, ok := userArr[0].(uint32); ok {
|
||||||
m.state.User = uid
|
m.state.User = uid
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@ func (m *Manager) updateSessionState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v, ok := props["Seat"]; ok {
|
if v, ok := props["Seat"]; ok {
|
||||||
if seatArr, ok := v.Value().([]interface{}); ok && len(seatArr) >= 1 {
|
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||||
if seatID, ok := seatArr[0].(string); ok {
|
if seatID, ok := seatArr[0].(string); ok {
|
||||||
m.state.Seat = seatID
|
m.state.Seat = seatID
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user