mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 15:32:50 -05:00
Compare commits
223 Commits
468e569bc7
...
hotfix-1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1926db95de | ||
|
|
5ad2a9d704 | ||
|
|
e0ab20dbda | ||
|
|
aadc3111a2 | ||
|
|
741d492084 | ||
|
|
604d55015c | ||
|
|
a4ce39caa5 | ||
|
|
0a82c9877d | ||
|
|
56f5c5eccb | ||
|
|
d20b5adbfa | ||
|
|
10dc86a5dc | ||
|
|
5463aed213 | ||
|
|
f435f0d413 | ||
|
|
521d804763 | ||
|
|
e203ec960a | ||
|
|
830ca10b45 | ||
|
|
4ffa06945a | ||
|
|
b1406fc49a | ||
|
|
f8179167a8 | ||
|
|
32998a5219 | ||
|
|
7fb358bada | ||
|
|
73cf3130e1 | ||
|
|
119b5df6df | ||
|
|
8ede810d32 | ||
|
|
830dd93af5 | ||
|
|
75f28c5ea7 | ||
|
|
6c9b8c590e | ||
|
|
24d9b77307 | ||
|
|
d4be68912c | ||
|
|
a443721000 | ||
|
|
786b097187 | ||
|
|
8ca60c7d2a | ||
|
|
406dc64aba | ||
|
|
af5d6a2015 | ||
|
|
61c6f509ae | ||
|
|
98769ecd88 | ||
|
|
8615950bd6 | ||
|
|
1bec8dfc48 | ||
|
|
460486fe25 | ||
|
|
318c50bc6c | ||
|
|
3e08bac7f3 | ||
|
|
c3d64ab185 | ||
|
|
2b73077b50 | ||
|
|
f953bd5488 | ||
|
|
f94011cf05 | ||
|
|
aeacf109eb | ||
|
|
e307de83e2 | ||
|
|
85968ec417 | ||
|
|
993f14a31f | ||
|
|
566d617508 | ||
|
|
542a279fcb | ||
|
|
e784bb89e1 | ||
|
|
f680ace258 | ||
|
|
7aa5976e07 | ||
|
|
f88f1ea951 | ||
|
|
da4561cb35 | ||
|
|
1f89ae9813 | ||
|
|
5647323449 | ||
|
|
bc27253cbf | ||
|
|
0672b711f3 | ||
|
|
ed9ee6e347 | ||
|
|
7ad23ad4a2 | ||
|
|
8a83f03cc1 | ||
|
|
0be9ac4097 | ||
|
|
ba5be6b516 | ||
|
|
c4aea6d326 | ||
|
|
858c6407a9 | ||
|
|
c4313395b5 | ||
|
|
a32aec3d59 | ||
|
|
696bcfe8fa | ||
|
|
2f3a253c6a | ||
|
|
e41fbe0188 | ||
|
|
ef9d28597b | ||
|
|
6f3c4c89ab | ||
|
|
60c577a61e | ||
|
|
f3276c3039 | ||
|
|
37a843323d | ||
|
|
95c780ca8c | ||
|
|
d60d5b154a | ||
|
|
0435a805c7 | ||
|
|
f406a977e0 | ||
|
|
18db1e1ecb | ||
|
|
6bd1beb719 | ||
|
|
1293aecbca | ||
|
|
8a10c2e112 | ||
|
|
c21d777269 | ||
|
|
d864094f48 | ||
|
|
deaac3fdf0 | ||
|
|
b7062fe40c | ||
|
|
64d5e99b9d | ||
|
|
f9d8a7d22b | ||
|
|
52fcd3ad98 | ||
|
|
9d1e0ee29b | ||
|
|
de62f48f50 | ||
|
|
f47b19274c | ||
|
|
bb7f7083b9 | ||
|
|
cd580090dc | ||
|
|
ddb74b598d | ||
|
|
29571fc3aa | ||
|
|
57ee0fb2bd | ||
|
|
3ef10e73a5 | ||
|
|
dc40492fc7 | ||
|
|
e606a76a86 | ||
|
|
8838fd67b9 | ||
|
|
c570e20308 | ||
|
|
0a00ef39e3 | ||
|
|
9a08b81214 | ||
|
|
c617ae26a2 | ||
|
|
f6a776a692 | ||
|
|
54b253099d | ||
|
|
f662aca58c | ||
|
|
76e7755496 | ||
|
|
e05ad81c13 | ||
|
|
cffb16d7f7 | ||
|
|
18ca571944 | ||
|
|
3ae1973e21 | ||
|
|
308c8c3ea7 | ||
|
|
f49b5dd037 | ||
|
|
f245ba82ad | ||
|
|
60d22d6973 | ||
|
|
d6f48a82d9 | ||
|
|
c0d73dae67 | ||
|
|
49eb60589d | ||
|
|
89993b7421 | ||
|
|
511cb93806 | ||
|
|
8ce78e7134 | ||
|
|
9ebfab2e78 | ||
|
|
833d245251 | ||
|
|
00d3024143 | ||
|
|
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 |
@@ -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
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
23
.github/workflows/nix-pr-check.yml
vendored
Normal file
23
.github/workflows/nix-pr-check.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Check nix flake
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
paths:
|
||||||
|
- "flake.*"
|
||||||
|
- "distro/nix/**"
|
||||||
|
jobs:
|
||||||
|
check-flake:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
- name: Check the flake
|
||||||
|
run: nix flake check
|
||||||
598
.github/workflows/release.yml
vendored
598
.github/workflows/release.yml
vendored
@@ -1,16 +1,19 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
inputs:
|
||||||
- 'v*'
|
tag:
|
||||||
|
description: "Tag to release (e.g., v1.0.1)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
actions: write
|
actions: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref_name }}
|
group: release-${{ inputs.tag }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -24,10 +27,14 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: core
|
working-directory: core
|
||||||
|
|
||||||
|
env:
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
@@ -54,7 +61,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dankinstall
|
cd cmd/dankinstall
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dankinstall-${{ matrix.arch }}
|
-o ../../dankinstall-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||||
@@ -68,7 +75,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-${{ matrix.arch }}
|
-o ../../dms-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-${{ matrix.arch }}
|
gzip -9 -k dms-${{ matrix.arch }}
|
||||||
@@ -91,7 +98,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||||
@@ -128,60 +135,61 @@ jobs:
|
|||||||
core/completion.zsh
|
core/completion.zsh
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
update-versions:
|
# update-versions:
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
needs: build-core
|
# needs: build-core
|
||||||
steps:
|
# steps:
|
||||||
- name: Create GitHub App token
|
# - name: Create GitHub App token
|
||||||
id: app_token
|
# id: app_token
|
||||||
uses: actions/create-github-app-token@v1
|
# uses: actions/create-github-app-token@v1
|
||||||
with:
|
# with:
|
||||||
app-id: ${{ secrets.APP_ID }}
|
# app-id: ${{ secrets.APP_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
# - name: Checkout
|
||||||
uses: actions/checkout@v4
|
# uses: actions/checkout@v4
|
||||||
with:
|
# with:
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
fetch-depth: 0
|
# fetch-depth: 0
|
||||||
|
|
||||||
- name: Update VERSION
|
# - name: Update VERSION
|
||||||
env:
|
# env:
|
||||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
run: |
|
# run: |
|
||||||
set -euo pipefail
|
# set -euo pipefail
|
||||||
git config user.name "dms-ci[bot]"
|
# git config user.name "dms-ci[bot]"
|
||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
# git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
version="${GITHUB_REF#refs/tags/}"
|
# version="${GITHUB_REF#refs/tags/}"
|
||||||
echo "Updating to version: $version"
|
# echo "Updating to version: $version"
|
||||||
echo "${version}" > quickshell/VERSION
|
# echo "${version}" > quickshell/VERSION
|
||||||
git add quickshell/VERSION
|
# git add quickshell/VERSION
|
||||||
|
|
||||||
if ! git diff --cached --quiet; then
|
# if ! git diff --cached --quiet; then
|
||||||
git commit -m "chore: bump version to $version"
|
# git commit -m "chore: bump version to $version"
|
||||||
git pull --rebase origin master
|
# git pull --rebase origin master
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
# git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
fi
|
# fi
|
||||||
|
|
||||||
git tag -f "${version}"
|
# git tag -f "${version}"
|
||||||
git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
# git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: [build-core, update-versions]
|
needs: [build-core] #, update-versions]
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Fetch updated tag after version bump
|
- name: Fetch updated tag after version bump
|
||||||
run: |
|
run: |
|
||||||
git fetch origin --force tag ${{ github.ref_name }}
|
git fetch origin --force tag ${TAG}
|
||||||
git checkout ${{ github.ref_name }}
|
git checkout ${TAG}
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -388,312 +396,296 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
trigger-obs-update:
|
# trigger-obs-update:
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
needs: release
|
# needs: release
|
||||||
steps:
|
# env:
|
||||||
- name: Checkout
|
# TAG: ${{ inputs.tag }}
|
||||||
uses: actions/checkout@v4
|
# steps:
|
||||||
|
# - name: Checkout
|
||||||
- name: Install OSC
|
# uses: actions/checkout@v4
|
||||||
run: |
|
# with:
|
||||||
sudo apt-get update
|
# ref: ${{ inputs.tag }}
|
||||||
sudo apt-get install -y osc
|
|
||||||
|
|
||||||
mkdir -p ~/.config/osc
|
|
||||||
cat > ~/.config/osc/oscrc << EOF
|
|
||||||
[general]
|
|
||||||
apiurl = https://api.opensuse.org
|
|
||||||
|
|
||||||
[https://api.opensuse.org]
|
|
||||||
user = ${{ secrets.OBS_USERNAME }}
|
|
||||||
pass = ${{ secrets.OBS_PASSWORD }}
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/osc/oscrc
|
|
||||||
|
|
||||||
- name: Update OBS packages
|
|
||||||
run: |
|
|
||||||
VERSION="${{ github.ref_name }}"
|
|
||||||
cd distro
|
|
||||||
bash scripts/obs-upload.sh dms "Update to $VERSION"
|
|
||||||
|
|
||||||
trigger-ppa-update:
|
# - name: Install OSC
|
||||||
runs-on: ubuntu-latest
|
# run: |
|
||||||
needs: release
|
# sudo apt-get update
|
||||||
steps:
|
# sudo apt-get install -y osc
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
debhelper \
|
|
||||||
devscripts \
|
|
||||||
dput \
|
|
||||||
lftp \
|
|
||||||
build-essential \
|
|
||||||
fakeroot \
|
|
||||||
dpkg-dev
|
|
||||||
|
|
||||||
- name: Configure GPG
|
|
||||||
env:
|
|
||||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
||||||
run: |
|
|
||||||
echo "$GPG_KEY" | gpg --import
|
|
||||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
|
||||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Upload to PPA
|
|
||||||
run: |
|
|
||||||
VERSION="${{ github.ref_name }}"
|
|
||||||
cd distro/ubuntu/ppa
|
|
||||||
bash create-and-upload.sh ../dms dms questing
|
|
||||||
|
|
||||||
copr-build:
|
# mkdir -p ~/.config/osc
|
||||||
runs-on: ubuntu-latest
|
# cat > ~/.config/osc/oscrc << EOF
|
||||||
needs: release
|
# [general]
|
||||||
env:
|
# apiurl = https://api.opensuse.org
|
||||||
TAG: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
steps:
|
# [https://api.opensuse.org]
|
||||||
- name: Checkout repository
|
# user = ${{ secrets.OBS_USERNAME }}
|
||||||
uses: actions/checkout@v4
|
# pass = ${{ secrets.OBS_PASSWORD }}
|
||||||
|
# EOF
|
||||||
|
# chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
- name: Determine version
|
# - name: Update OBS packages
|
||||||
id: version
|
# run: |
|
||||||
run: |
|
# cd distro
|
||||||
VERSION="${TAG#v}"
|
# bash scripts/obs-upload.sh dms "Update to ${TAG}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Building DMS stable version: $VERSION"
|
|
||||||
|
|
||||||
- name: Setup build environment
|
# trigger-ppa-update:
|
||||||
run: |
|
# runs-on: ubuntu-latest
|
||||||
sudo apt-get update
|
# needs: release
|
||||||
sudo apt-get install -y rpm wget curl jq gzip
|
# env:
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
# TAG: ${{ inputs.tag }}
|
||||||
|
# steps:
|
||||||
|
# - name: Checkout
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Download release assets
|
# - name: Install build dependencies
|
||||||
run: |
|
# run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
# sudo apt-get update
|
||||||
cd ~/rpmbuild/SOURCES
|
# sudo apt-get install -y \
|
||||||
|
# debhelper \
|
||||||
|
# devscripts \
|
||||||
|
# dput \
|
||||||
|
# lftp \
|
||||||
|
# build-essential \
|
||||||
|
# fakeroot \
|
||||||
|
# dpkg-dev
|
||||||
|
|
||||||
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
# - name: Configure GPG
|
||||||
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
# env:
|
||||||
exit 1
|
# GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
}
|
# run: |
|
||||||
|
# echo "$GPG_KEY" | gpg --import
|
||||||
|
# GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
|
# echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Generate stable spec file
|
# - name: Upload to PPA
|
||||||
run: |
|
# run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
# cd distro/ubuntu/ppa
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
# bash create-and-upload.sh ../dms dms questing
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
# copr-build:
|
||||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: release
|
||||||
|
# env:
|
||||||
|
# TAG: ${{ inputs.tag }}
|
||||||
|
|
||||||
%global debug_package %{nil}
|
# steps:
|
||||||
%global version VERSION_PLACEHOLDER
|
# - name: Checkout repository
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
# uses: actions/checkout@v4
|
||||||
|
# with:
|
||||||
|
# ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
Name: dms
|
# - name: Determine version
|
||||||
Version: %{version}
|
# id: version
|
||||||
Release: 1%{?dist}
|
# run: |
|
||||||
Summary: %{pkg_summary}
|
# VERSION="${TAG#v}"
|
||||||
|
# echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
# echo "Building DMS stable version: $VERSION"
|
||||||
|
|
||||||
License: MIT
|
# - name: Setup build environment
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
# run: |
|
||||||
|
# sudo apt-get update
|
||||||
|
# sudo apt-get install -y rpm wget curl jq gzip
|
||||||
|
# mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
# - name: Download release assets
|
||||||
|
# run: |
|
||||||
|
# VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
# cd ~/rpmbuild/SOURCES
|
||||||
|
|
||||||
BuildRequires: gzip
|
# wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||||
BuildRequires: wget
|
# echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||||
BuildRequires: systemd-rpm-macros
|
# exit 1
|
||||||
|
# }
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
# - name: Generate stable spec file
|
||||||
Requires: accountsservice
|
# run: |
|
||||||
Requires: dms-cli
|
# VERSION="${{ steps.version.outputs.version }}"
|
||||||
Requires: dgop
|
# CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
|
||||||
Recommends: cava
|
# cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
||||||
Recommends: cliphist
|
# # Spec for DMS stable releases - Generated by GitHub Actions
|
||||||
Recommends: danksearch
|
|
||||||
Recommends: hyprpicker
|
|
||||||
Recommends: matugen
|
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
|
||||||
Recommends: qt6-qtmultimedia
|
|
||||||
Suggests: qt6ct
|
|
||||||
|
|
||||||
%description
|
# %global debug_package %{nil}
|
||||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
# %global version VERSION_PLACEHOLDER
|
||||||
and optimized for the niri and hyprland compositors. Features notifications,
|
# %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
# Name: dms
|
||||||
process monitoring, notification center, clipboard history, dock, control center,
|
# Version: %{version}
|
||||||
lock screen, and comprehensive plugin system.
|
# Release: 1%{?dist}
|
||||||
|
# Summary: %{pkg_summary}
|
||||||
|
|
||||||
%package -n dms-cli
|
# License: MIT
|
||||||
Summary: DankMaterialShell CLI tool
|
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
%description -n dms-cli
|
# Source0: dms-qml.tar.gz
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
%package -n dgop
|
# BuildRequires: gzip
|
||||||
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
# BuildRequires: wget
|
||||||
License: MIT
|
# BuildRequires: systemd-rpm-macros
|
||||||
URL: https://github.com/AvengeMedia/dgop
|
|
||||||
Provides: dgop
|
|
||||||
|
|
||||||
%description -n dgop
|
# Requires: (quickshell or quickshell-git)
|
||||||
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
# Requires: accountsservice
|
||||||
network statistics. Designed for integration with DankMaterialShell but can be
|
# Requires: dms-cli = %{version}-%{release}
|
||||||
used standalone. This package always includes the latest stable dgop release.
|
# Requires: dgop
|
||||||
|
|
||||||
%prep
|
# Recommends: cava
|
||||||
%setup -q -c -n dms-qml
|
# Recommends: cliphist
|
||||||
|
# Recommends: danksearch
|
||||||
|
# Recommends: matugen
|
||||||
|
# Recommends: wl-clipboard
|
||||||
|
# Recommends: NetworkManager
|
||||||
|
# Recommends: qt6-qtmultimedia
|
||||||
|
# Suggests: qt6ct
|
||||||
|
|
||||||
# Download architecture-specific binaries during build
|
# %description
|
||||||
case "%{_arch}" in
|
# DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
||||||
x86_64)
|
# and optimized for the niri and hyprland compositors. Features notifications,
|
||||||
ARCH_SUFFIX="amd64"
|
# app launcher, wallpaper customization, and fully customizable with plugins.
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
ARCH_SUFFIX="arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: %{_arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
# Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
# process monitoring, notification center, clipboard history, dock, control center,
|
||||||
exit 1
|
# lock screen, and comprehensive plugin system.
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
# %package -n dms-cli
|
||||||
echo "Failed to download dgop for architecture %{_arch}"
|
# Summary: DankMaterialShell CLI tool
|
||||||
exit 1
|
# License: MIT
|
||||||
}
|
# URL: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
|
||||||
chmod +x %{_builddir}/dgop
|
|
||||||
|
|
||||||
%build
|
# %description -n dms-cli
|
||||||
|
# Command-line interface for DankMaterialShell configuration and management.
|
||||||
|
# Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||||
|
|
||||||
%install
|
# %prep
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
# %setup -q -c -n dms-qml
|
||||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
|
||||||
|
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
# # Download architecture-specific binaries during build
|
||||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
# case "%{_arch}" in
|
||||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
# x86_64)
|
||||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
# ARCH_SUFFIX="amd64"
|
||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
# ;;
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
# aarch64)
|
||||||
|
# ARCH_SUFFIX="arm64"
|
||||||
|
# ;;
|
||||||
|
# *)
|
||||||
|
# echo "Unsupported architecture: %{_arch}"
|
||||||
|
# exit 1
|
||||||
|
# ;;
|
||||||
|
# esac
|
||||||
|
|
||||||
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
# wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||||
|
# echo "Failed to download dms-cli for architecture %{_arch}"
|
||||||
|
# exit 1
|
||||||
|
# }
|
||||||
|
# gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||||
|
# chmod +x %{_builddir}/dms-cli
|
||||||
|
|
||||||
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
# %build
|
||||||
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
# %install
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
# install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
# install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
# install -d %{buildroot}%{_datadir}/zsh/site-functions
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
# install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
# %{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/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 || :
|
||||||
|
|
||||||
%posttrans
|
# install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$1" -ge 2 ]; then
|
# install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
# install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
fi
|
|
||||||
|
|
||||||
%files
|
# install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
||||||
%license LICENSE
|
# cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
||||||
%doc README.md CONTRIBUTING.md
|
|
||||||
%{_datadir}/quickshell/dms/
|
|
||||||
%{_userunitdir}/dms.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
%files -n dms-cli
|
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
||||||
%{_bindir}/dms
|
# rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
||||||
%{_datadir}/bash-completion/completions/dms
|
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
||||||
%{_datadir}/zsh/site-functions/_dms
|
# rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
%files -n dgop
|
# echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
||||||
%{_bindir}/dgop
|
|
||||||
|
|
||||||
%changelog
|
# %posttrans
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
# if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
||||||
- Stable release VERSION_PLACEHOLDER
|
# rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
||||||
- Built from GitHub release
|
# rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||||
- Includes latest dms-cli and dgop binaries
|
# rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||||
SPECEOF
|
# fi
|
||||||
|
# # Signal running DMS instances to reload
|
||||||
|
# pkill -USR1 -x dms >/dev/null 2>&1 || :
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
# %files
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
# %license LICENSE
|
||||||
|
# %doc README.md CONTRIBUTING.md
|
||||||
|
# %{_datadir}/quickshell/dms/
|
||||||
|
# %{_userunitdir}/dms.service
|
||||||
|
# %{_datadir}/applications/dms-open.desktop
|
||||||
|
# %{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
- name: Build SRPM
|
# %files -n dms-cli
|
||||||
id: build
|
# %{_bindir}/dms
|
||||||
run: |
|
# %{_datadir}/bash-completion/completions/dms
|
||||||
cd ~/rpmbuild/SPECS
|
# %{_datadir}/zsh/site-functions/_dms
|
||||||
rpmbuild -bs dms.spec
|
# %{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
# %changelog
|
||||||
SRPM_NAME=$(basename "$SRPM")
|
# * CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
||||||
|
# - Stable release VERSION_PLACEHOLDER
|
||||||
|
# - Built from GitHub release
|
||||||
|
# SPECEOF
|
||||||
|
|
||||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
# sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
# sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
echo "SRPM built: $SRPM_NAME"
|
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
# - name: Build SRPM
|
||||||
uses: actions/upload-artifact@v4
|
# id: build
|
||||||
with:
|
# run: |
|
||||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
# cd ~/rpmbuild/SPECS
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
# rpmbuild -bs dms.spec
|
||||||
retention-days: 90
|
|
||||||
|
|
||||||
- name: Install Copr CLI
|
# SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
||||||
run: |
|
# SRPM_NAME=$(basename "$SRPM")
|
||||||
sudo apt-get install -y python3-pip
|
|
||||||
pip3 install copr-cli
|
|
||||||
|
|
||||||
mkdir -p ~/.config
|
# echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
cat > ~/.config/copr << EOF
|
# echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||||
[copr-cli]
|
# echo "SRPM built: $SRPM_NAME"
|
||||||
login = ${{ secrets.COPR_LOGIN }}
|
|
||||||
username = avengemedia
|
|
||||||
token = ${{ secrets.COPR_TOKEN }}
|
|
||||||
copr_url = https://copr.fedorainfracloud.org
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/copr
|
|
||||||
|
|
||||||
- name: Upload to Copr
|
# - name: Upload SRPM artifact
|
||||||
run: |
|
# uses: actions/upload-artifact@v4
|
||||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
# with:
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
# name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
|
# path: ${{ steps.build.outputs.srpm_path }}
|
||||||
|
# retention-days: 90
|
||||||
|
|
||||||
echo "Uploading SRPM to avengemedia/dms..."
|
# - name: Install Copr CLI
|
||||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
# run: |
|
||||||
echo "$BUILD_OUTPUT"
|
# sudo apt-get install -y python3-pip
|
||||||
|
# pip3 install copr-cli
|
||||||
|
|
||||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
# mkdir -p ~/.config
|
||||||
|
# cat > ~/.config/copr << EOF
|
||||||
|
# [copr-cli]
|
||||||
|
# login = ${{ secrets.COPR_LOGIN }}
|
||||||
|
# username = avengemedia
|
||||||
|
# token = ${{ secrets.COPR_TOKEN }}
|
||||||
|
# copr_url = https://copr.fedorainfracloud.org
|
||||||
|
# EOF
|
||||||
|
# chmod 600 ~/.config/copr
|
||||||
|
|
||||||
if [ "$BUILD_ID" != "unknown" ]; then
|
# - name: Upload to Copr
|
||||||
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
# run: |
|
||||||
fi
|
# SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
|
# VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
# echo "Uploading SRPM to avengemedia/dms..."
|
||||||
|
# BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
||||||
|
# echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
|
# BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
|
# if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
|
# echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
||||||
|
# fi
|
||||||
|
|||||||
35
.github/workflows/run-copr.yml
vendored
35
.github/workflows/run-copr.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo "✅ Source downloaded"
|
echo "✅ Source downloaded"
|
||||||
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture"
|
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
|
||||||
ls -lh
|
ls -lh
|
||||||
|
|
||||||
- name: Generate stable spec file
|
- name: Generate stable spec file
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
Requires: (quickshell or quickshell-git)
|
||||||
Requires: accountsservice
|
Requires: accountsservice
|
||||||
Requires: dms-cli
|
Requires: dms-cli = %{version}-%{release}
|
||||||
Requires: dgop
|
Requires: dgop
|
||||||
|
|
||||||
Recommends: cava
|
Recommends: cava
|
||||||
@@ -125,17 +125,6 @@ jobs:
|
|||||||
Command-line interface for DankMaterialShell configuration and management.
|
Command-line interface for DankMaterialShell configuration and management.
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
||||||
|
|
||||||
%package -n dgop
|
|
||||||
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/dgop
|
|
||||||
Provides: dgop
|
|
||||||
|
|
||||||
%description -n dgop
|
|
||||||
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
|
||||||
network statistics. Designed for integration with DankMaterialShell but can be
|
|
||||||
used standalone. This package always includes the latest stable dgop release.
|
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
%setup -q -c -n dms-qml
|
%setup -q -c -n dms-qml
|
||||||
|
|
||||||
@@ -162,19 +151,10 @@ jobs:
|
|||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
||||||
chmod +x %{_builddir}/dms-cli
|
chmod +x %{_builddir}/dms-cli
|
||||||
|
|
||||||
# Download dgop for target architecture
|
|
||||||
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dgop for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
|
||||||
chmod +x %{_builddir}/dgop
|
|
||||||
|
|
||||||
%build
|
%build
|
||||||
|
|
||||||
%install
|
%install
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
||||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
|
||||||
|
|
||||||
# Shell completions
|
# Shell completions
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
||||||
@@ -202,11 +182,8 @@ jobs:
|
|||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
# Signal running DMS instances to reload (harmless if none running)
|
||||||
# Restart DMS for active users after upgrade
|
pkill -USR1 -x dms >/dev/null 2>&1 || :
|
||||||
if [ "$1" -ge 2 ]; then
|
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
%files
|
%files
|
||||||
%license LICENSE
|
%license LICENSE
|
||||||
@@ -220,14 +197,10 @@ jobs:
|
|||||||
%{_datadir}/zsh/site-functions/_dms
|
%{_datadir}/zsh/site-functions/_dms
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
%{_datadir}/fish/vendor_completions.d/dms.fish
|
||||||
|
|
||||||
%files -n dgop
|
|
||||||
%{_bindir}/dgop
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
||||||
- Stable release VERSION_PLACEHOLDER
|
- Stable release VERSION_PLACEHOLDER
|
||||||
- Built from GitHub release
|
- Built from GitHub release
|
||||||
- Includes latest dms-cli and dgop binaries
|
|
||||||
SPECEOF
|
SPECEOF
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
||||||
|
|||||||
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
|
||||||
|
|||||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -102,39 +102,6 @@ go.work.sum
|
|||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
|
||||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
|
||||||
#
|
|
||||||
# Binaries for programs and plugins
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Code coverage profiles and other test artifacts
|
|
||||||
*.out
|
|
||||||
coverage.*
|
|
||||||
*.coverprofile
|
|
||||||
profile.cov
|
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
# vendor/
|
|
||||||
|
|
||||||
# Go workspace file
|
|
||||||
go.work
|
|
||||||
go.work.sum
|
|
||||||
|
|
||||||
# env file
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Editor/IDE
|
|
||||||
# .idea/
|
|
||||||
# .vscode/
|
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
# Extracted source trees in Ubuntu package directories
|
# Extracted source trees in Ubuntu package directories
|
||||||
@@ -142,3 +109,7 @@ distro/ubuntu/*/dms-git-repo/
|
|||||||
distro/ubuntu/*/DankMaterialShell-*/
|
distro/ubuntu/*/DankMaterialShell-*/
|
||||||
distro/ubuntu/danklinux/*/dsearch-*/
|
distro/ubuntu/danklinux/*/dsearch-*/
|
||||||
distro/ubuntu/danklinux/*/dgop-*/
|
distro/ubuntu/danklinux/*/dgop-*/
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.envrc
|
||||||
|
.direnv/
|
||||||
|
|||||||
@@ -12,6 +12,21 @@ Enable pre-commit hooks to catch CI failures before pushing:
|
|||||||
git config core.hooksPath .githooks
|
git config core.hooksPath .githooks
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Nix Development Shell
|
||||||
|
|
||||||
|
If you have Nix installed with flakes enabled, you can use the provided development shell which includes all necessary dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix develop
|
||||||
|
```
|
||||||
|
|
||||||
|
This will provide:
|
||||||
|
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||||
|
- Quickshell and required QML packages
|
||||||
|
- Properly configured QML2_IMPORT_PATH
|
||||||
|
|
||||||
|
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
||||||
|
|
||||||
## VSCode Setup
|
## VSCode Setup
|
||||||
|
|
||||||
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -43,6 +43,7 @@ install-shell:
|
|||||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
@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"
|
@echo "Shell files installed"
|
||||||
|
|
||||||
install-completions:
|
install-completions:
|
||||||
@@ -80,8 +81,7 @@ install: build install-bin install-shell install-completions install-systemd ins
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "To enable and start DMS:"
|
@echo "=== Cheers, the DMS Team! ==="
|
||||||
@echo " systemctl --user enable --now dms"
|
|
||||||
|
|
||||||
# Uninstallation targets
|
# Uninstallation targets
|
||||||
uninstall-bin:
|
uninstall-bin:
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -5,21 +5,21 @@
|
|||||||
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### A modern desktop shell for Wayland
|
### A modern desktop shell for Wayland
|
||||||
|
|
||||||
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
||||||
|
|
||||||
[](https://danklinux.com/docs)
|
[](https://danklinux.com/docs)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||||
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||||
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
[](https://ko-fi.com/danklinux)
|
[](https://ko-fi.com/danklinux)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
|||||||
|
|
||||||
## Supported Compositors
|
## Supported Compositors
|
||||||
|
|
||||||
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), and [labwc](https://labwc.github.io/) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
||||||
|
|
||||||
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **Website:** [danklinux.com](https://danklinux.com)
|
- **Website:** [danklinux.com](https://danklinux.com)
|
||||||
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs)
|
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs/)
|
||||||
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
||||||
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
||||||
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||||
@@ -143,6 +143,7 @@ See component-specific documentation:
|
|||||||
### Building from Source
|
### Building from Source
|
||||||
|
|
||||||
**Core + Dankinstall:**
|
**Core + Dankinstall:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd core
|
cd core
|
||||||
make # Build dms CLI
|
make # Build dms CLI
|
||||||
@@ -150,11 +151,13 @@ make dankinstall # Build installer
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Shell:**
|
**Shell:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
quickshell -p quickshell/
|
quickshell -p quickshell/
|
||||||
```
|
```
|
||||||
|
|
||||||
**NixOS:**
|
**NixOS:**
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
indentation = "FourSpaces"
|
|
||||||
@@ -6,5 +6,5 @@ Exec=dms open %u
|
|||||||
Icon=danklogo
|
Icon=danklogo
|
||||||
Terminal=false
|
Terminal=false
|
||||||
NoDisplay=true
|
NoDisplay=true
|
||||||
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/file;text/html;
|
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
|
||||||
Categories=Utility;
|
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
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ linters:
|
|||||||
# Signal handling
|
# Signal handling
|
||||||
- (*os.Process).Signal
|
- (*os.Process).Signal
|
||||||
- (*os.Process).Kill
|
- (*os.Process).Kill
|
||||||
|
- syscall.Kill
|
||||||
|
# Seek on memfd (reset position before passing fd)
|
||||||
|
- syscall.Seek
|
||||||
# DBus cleanup
|
# DBus cleanup
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
||||||
|
|||||||
@@ -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+
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if os.Getuid() == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
fileLogger, err := log.NewFileLogger()
|
fileLogger, err := log.NewFileLogger()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -192,45 +211,13 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
exponential, _ := cmd.Flags().GetBool("exponential")
|
exponential, _ := cmd.Flags().GetBool("exponential")
|
||||||
exponent, _ := cmd.Flags().GetFloat64("exponent")
|
exponent, _ := cmd.Flags().GetFloat64("exponent")
|
||||||
|
|
||||||
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
|
|
||||||
parts := strings.SplitN(deviceID, ":", 2)
|
parts := strings.SplitN(deviceID, ":", 2)
|
||||||
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
|
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
|
||||||
subsystem := parts[0]
|
if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok {
|
||||||
name := parts[1]
|
return
|
||||||
|
|
||||||
// Initialize backends needed for logind approach
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("NewSysfsBackend failed: %v", err)
|
|
||||||
} else {
|
|
||||||
logind, err := brightness.NewLogindBackend()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("NewLogindBackend failed: %v", err)
|
|
||||||
} else {
|
|
||||||
defer logind.Close()
|
|
||||||
|
|
||||||
// Get device info to convert percent to value
|
|
||||||
dev, err := sysfs.GetDevice(deviceID)
|
|
||||||
if err == nil {
|
|
||||||
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
|
|
||||||
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
|
|
||||||
|
|
||||||
// Call logind with hardware value
|
|
||||||
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
|
|
||||||
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
|
|
||||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
log.Debugf("logind.SetBrightness failed: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to direct sysfs (requires write permissions)
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
|
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
|
||||||
@@ -261,31 +248,51 @@ 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 tryLogindBrightness(subsystem, name, deviceID string, percent int, exponential bool, exponent float64) bool {
|
||||||
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("NewSysfsBackend failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logind, err := brightness.NewLogindBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("NewLogindBackend failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer logind.Close()
|
||||||
|
|
||||||
|
dev, err := sysfs.GetDevice(deviceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
|
||||||
|
if err := logind.SetBrightness(subsystem, name, uint32(value)); err != nil {
|
||||||
|
log.Debugf("logind.SetBrightness failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
|
||||||
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -134,9 +152,75 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pluginsUpdateCmd = &cobra.Command{
|
||||||
|
Use: "update <plugin-id>",
|
||||||
|
Short: "Update a plugin by ID",
|
||||||
|
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
|
||||||
|
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) {
|
||||||
|
if err := updatePluginCLI(args[0]); err != nil {
|
||||||
|
log.Fatalf("Error updating plugin: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
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 +383,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 {
|
||||||
@@ -310,53 +426,73 @@ func uninstallPluginCLI(idOrName string) error {
|
|||||||
return fmt.Errorf("failed to create registry: %w", err)
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginList, err := registry.List()
|
pluginList, _ := registry.List()
|
||||||
if err != nil {
|
plugin := plugins.FindByIDOrName(idOrName, pluginList)
|
||||||
return fmt.Errorf("failed to list plugins: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, try to find by ID (preferred method)
|
if plugin != nil {
|
||||||
var plugin *plugins.Plugin
|
installed, err := manager.IsInstalled(*plugin)
|
||||||
for _, p := range pluginList {
|
if err != nil {
|
||||||
if p.ID == idOrName {
|
return fmt.Errorf("failed to check install status: %w", err)
|
||||||
plugin = &p
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
if !installed {
|
||||||
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
// Fallback to name for backward compatibility
|
|
||||||
if plugin == nil {
|
|
||||||
for _, p := range pluginList {
|
|
||||||
if p.Name == idOrName {
|
|
||||||
plugin = &p
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
if err := manager.Uninstall(*plugin); err != nil {
|
||||||
|
return fmt.Errorf("failed to uninstall plugin: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if plugin == nil {
|
fmt.Printf("Uninstalling plugin: %s\n", idOrName)
|
||||||
return fmt.Errorf("plugin not found: %s", idOrName)
|
if err := manager.UninstallByIDOrName(idOrName); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName)
|
||||||
installed, err := manager.IsInstalled(*plugin)
|
return nil
|
||||||
if err != nil {
|
}
|
||||||
return fmt.Errorf("failed to check install status: %w", err)
|
|
||||||
}
|
func updatePluginCLI(idOrName string) error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
if !installed {
|
if err != nil {
|
||||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
registry, err := plugins.NewRegistry()
|
||||||
if err := manager.Uninstall(*plugin); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to uninstall plugin: %w", err)
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
|
pluginList, _ := registry.List()
|
||||||
|
plugin := plugins.FindByIDOrName(idOrName, pluginList)
|
||||||
|
|
||||||
|
if plugin != nil {
|
||||||
|
installed, err := manager.IsInstalled(*plugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check install status: %w", err)
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
if err := manager.Update(*plugin); err != nil {
|
||||||
|
return fmt.Errorf("failed to update plugin: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updating plugin: %s\n", idOrName)
|
||||||
|
if err := manager.UpdateByIDOrName(idOrName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", idOrName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCommonCommands returns the commands available in all builds
|
|
||||||
func getCommonCommands() []*cobra.Command {
|
func getCommonCommands() []*cobra.Command {
|
||||||
return []*cobra.Command{
|
return []*cobra.Command{
|
||||||
versionCmd,
|
versionCmd,
|
||||||
@@ -373,5 +509,9 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
keybindsCmd,
|
keybindsCmd,
|
||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
|
colorCmd,
|
||||||
|
screenshotCmd,
|
||||||
|
notifyActionCmd,
|
||||||
|
matugenCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var dank16Cmd = &cobra.Command{
|
var dank16Cmd = &cobra.Command{
|
||||||
Use: "dank16 <hex_color>",
|
Use: "dank16 [hex_color]",
|
||||||
Short: "Generate Base16 color palettes",
|
Short: "Generate Base16 color palettes",
|
||||||
Long: "Generate Base16 color palettes from a color with support for various output formats",
|
Long: "Generate Base16 color palettes from a color with support for various output formats",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
Run: runDank16,
|
Run: runDank16,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant")
|
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant (sets default to light)")
|
||||||
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
||||||
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||||
@@ -27,14 +27,15 @@ 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.Flags().Bool("variants", false, "Output all variants (dark/light/default) in JSON")
|
||||||
|
dank16Cmd.Flags().String("primary-dark", "", "Primary color for dark mode (use with --variants)")
|
||||||
|
dank16Cmd.Flags().String("primary-light", "", "Primary color for light mode (use with --variants)")
|
||||||
|
_ = 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) {
|
||||||
primaryColor := args[0]
|
|
||||||
if !strings.HasPrefix(primaryColor, "#") {
|
|
||||||
primaryColor = "#" + primaryColor
|
|
||||||
}
|
|
||||||
|
|
||||||
isLight, _ := cmd.Flags().GetBool("light")
|
isLight, _ := cmd.Flags().GetBool("light")
|
||||||
isJson, _ := cmd.Flags().GetBool("json")
|
isJson, _ := cmd.Flags().GetBool("json")
|
||||||
isKitty, _ := cmd.Flags().GetBool("kitty")
|
isKitty, _ := cmd.Flags().GetBool("kitty")
|
||||||
@@ -44,16 +45,57 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||||
background, _ := cmd.Flags().GetString("background")
|
background, _ := cmd.Flags().GetString("background")
|
||||||
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
||||||
|
useVariants, _ := cmd.Flags().GetBool("variants")
|
||||||
|
primaryDark, _ := cmd.Flags().GetString("primary-dark")
|
||||||
|
primaryLight, _ := cmd.Flags().GetString("primary-light")
|
||||||
|
|
||||||
if background != "" && !strings.HasPrefix(background, "#") {
|
if background != "" && !strings.HasPrefix(background, "#") {
|
||||||
background = "#" + background
|
background = "#" + background
|
||||||
}
|
}
|
||||||
|
if primaryDark != "" && !strings.HasPrefix(primaryDark, "#") {
|
||||||
|
primaryDark = "#" + primaryDark
|
||||||
|
}
|
||||||
|
if primaryLight != "" && !strings.HasPrefix(primaryLight, "#") {
|
||||||
|
primaryLight = "#" + primaryLight
|
||||||
|
}
|
||||||
|
|
||||||
contrastAlgo = strings.ToLower(contrastAlgo)
|
contrastAlgo = strings.ToLower(contrastAlgo)
|
||||||
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
|
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
|
||||||
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
|
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if useVariants {
|
||||||
|
if primaryDark == "" || primaryLight == "" {
|
||||||
|
if len(args) == 0 {
|
||||||
|
log.Fatalf("--variants requires either a positional color argument or both --primary-dark and --primary-light")
|
||||||
|
}
|
||||||
|
primaryColor := args[0]
|
||||||
|
if !strings.HasPrefix(primaryColor, "#") {
|
||||||
|
primaryColor = "#" + primaryColor
|
||||||
|
}
|
||||||
|
primaryDark = primaryColor
|
||||||
|
primaryLight = primaryColor
|
||||||
|
}
|
||||||
|
variantOpts := dank16.VariantOptions{
|
||||||
|
PrimaryDark: primaryDark,
|
||||||
|
PrimaryLight: primaryLight,
|
||||||
|
Background: background,
|
||||||
|
UseDPS: contrastAlgo == "dps",
|
||||||
|
IsLightMode: isLight,
|
||||||
|
}
|
||||||
|
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||||
|
fmt.Print(dank16.GenerateVariantJSON(variantColors))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
log.Fatalf("A color argument is required (or use --variants with --primary-dark and --primary-light)")
|
||||||
|
}
|
||||||
|
primaryColor := args[0]
|
||||||
|
if !strings.HasPrefix(primaryColor, "#") {
|
||||||
|
primaryColor = "#" + primaryColor
|
||||||
|
}
|
||||||
|
|
||||||
opts := dank16.PaletteOptions{
|
opts := dank16.PaletteOptions{
|
||||||
IsLight: isLight,
|
IsLight: isLight,
|
||||||
Background: background,
|
Background: background,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -121,10 +122,10 @@ func updateArchLinux() error {
|
|||||||
var helper string
|
var helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
if commandExists("yay") {
|
if utils.CommandExists("yay") {
|
||||||
helper = "yay"
|
helper = "yay"
|
||||||
updateCmd = exec.Command("yay", "-S", packageName)
|
updateCmd = exec.Command("yay", "-S", packageName)
|
||||||
} else if commandExists("paru") {
|
} else if utils.CommandExists("paru") {
|
||||||
helper = "paru"
|
helper = "paru"
|
||||||
updateCmd = exec.Command("paru", "-S", packageName)
|
updateCmd = exec.Command("paru", "-S", packageName)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
@@ -448,7 +449,7 @@ func enableGreeter() error {
|
|||||||
fmt.Println("Detecting installed compositors...")
|
fmt.Println("Detecting installed compositors...")
|
||||||
compositors := greeter.DetectCompositors()
|
compositors := greeter.DetectCompositors()
|
||||||
|
|
||||||
if commandExists("sway") {
|
if utils.CommandExists("sway") {
|
||||||
compositors = append(compositors, "sway")
|
compositors = append(compositors, "sway")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -59,6 +89,11 @@ func initializeProviders() {
|
|||||||
log.Warnf("Failed to register MangoWC provider: %v", err)
|
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
|
||||||
|
if err := registry.Register(scrollProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Scroll provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
||||||
if err := registry.Register(swayProvider); err != nil {
|
if err := registry.Register(swayProvider); err != nil {
|
||||||
log.Warnf("Failed to register Sway provider: %v", err)
|
log.Warnf("Failed to register Sway provider: %v", err)
|
||||||
@@ -75,69 +110,124 @@ func initializeProviders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 "scroll":
|
||||||
provider = providers.NewHyprlandProvider(customPath)
|
return providers.NewSwayProvider(path)
|
||||||
case "mangowc":
|
case "niri":
|
||||||
provider = providers.NewMangoWCProvider(customPath)
|
return providers.NewNiriProvider(path)
|
||||||
case "sway":
|
default:
|
||||||
provider = providers.NewSwayProvider(customPath)
|
return nil
|
||||||
case "niri":
|
|
||||||
provider = providers.NewNiriProvider(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))
|
||||||
}
|
}
|
||||||
|
|||||||
182
core/cmd/dms/commands_matugen.go
Normal file
182
core/cmd/dms/commands_matugen.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var matugenCmd = &cobra.Command{
|
||||||
|
Use: "matugen",
|
||||||
|
Short: "Generate Material Design themes",
|
||||||
|
Long: "Generate Material Design themes using matugen with dank16 color integration",
|
||||||
|
}
|
||||||
|
|
||||||
|
var matugenGenerateCmd = &cobra.Command{
|
||||||
|
Use: "generate",
|
||||||
|
Short: "Generate theme synchronously",
|
||||||
|
Run: runMatugenGenerate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var matugenQueueCmd = &cobra.Command{
|
||||||
|
Use: "queue",
|
||||||
|
Short: "Queue theme generation (uses socket if available)",
|
||||||
|
Run: runMatugenQueue,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||||
|
matugenCmd.AddCommand(matugenQueueCmd)
|
||||||
|
|
||||||
|
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
||||||
|
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
||||||
|
cmd.Flags().String("shell-dir", "", "DMS shell installation directory")
|
||||||
|
cmd.Flags().String("config-dir", "", "User config directory")
|
||||||
|
cmd.Flags().String("kind", "image", "Source type: image or hex")
|
||||||
|
cmd.Flags().String("value", "", "Wallpaper path or hex color")
|
||||||
|
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
|
||||||
|
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
|
||||||
|
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
|
||||||
|
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
|
||||||
|
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
||||||
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
|
}
|
||||||
|
|
||||||
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
|
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
|
stateDir, _ := cmd.Flags().GetString("state-dir")
|
||||||
|
shellDir, _ := cmd.Flags().GetString("shell-dir")
|
||||||
|
configDir, _ := cmd.Flags().GetString("config-dir")
|
||||||
|
kind, _ := cmd.Flags().GetString("kind")
|
||||||
|
value, _ := cmd.Flags().GetString("value")
|
||||||
|
mode, _ := cmd.Flags().GetString("mode")
|
||||||
|
iconTheme, _ := cmd.Flags().GetString("icon-theme")
|
||||||
|
matugenType, _ := cmd.Flags().GetString("matugen-type")
|
||||||
|
runUserTemplates, _ := cmd.Flags().GetBool("run-user-templates")
|
||||||
|
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
||||||
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
|
|
||||||
|
return matugen.Options{
|
||||||
|
StateDir: stateDir,
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: configDir,
|
||||||
|
Kind: kind,
|
||||||
|
Value: value,
|
||||||
|
Mode: mode,
|
||||||
|
IconTheme: iconTheme,
|
||||||
|
MatugenType: matugenType,
|
||||||
|
RunUserTemplates: runUserTemplates,
|
||||||
|
StockColors: stockColors,
|
||||||
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
|
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||||
|
opts := buildMatugenOptions(cmd)
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||||
|
opts := buildMatugenOptions(cmd)
|
||||||
|
wait, _ := cmd.Flags().GetBool("wait")
|
||||||
|
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||||
|
|
||||||
|
socketPath := os.Getenv("DMS_SOCKET")
|
||||||
|
if socketPath == "" {
|
||||||
|
var err error
|
||||||
|
socketPath, err = server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Info("No socket available, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Socket connection failed, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
request := map[string]any{
|
||||||
|
"id": 1,
|
||||||
|
"method": "matugen.queue",
|
||||||
|
"params": map[string]any{
|
||||||
|
"stateDir": opts.StateDir,
|
||||||
|
"shellDir": opts.ShellDir,
|
||||||
|
"configDir": opts.ConfigDir,
|
||||||
|
"kind": opts.Kind,
|
||||||
|
"value": opts.Value,
|
||||||
|
"mode": opts.Mode,
|
||||||
|
"iconTheme": opts.IconTheme,
|
||||||
|
"matugenType": opts.MatugenType,
|
||||||
|
"runUserTemplates": opts.RunUserTemplates,
|
||||||
|
"stockColors": opts.StockColors,
|
||||||
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
|
"wait": wait,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(conn).Encode(request); err != nil {
|
||||||
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wait {
|
||||||
|
fmt.Println("Theme generation queued")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resultCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
var response struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Result any `json:"result"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(conn).Decode(&response); err != nil {
|
||||||
|
resultCh <- fmt.Errorf("failed to read response: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if response.Error != "" {
|
||||||
|
resultCh <- fmt.Errorf("server error: %s", response.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultCh <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-resultCh:
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Theme generation completed")
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Fatalf("Timeout waiting for theme generation")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,9 @@ func init() {
|
|||||||
openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications")
|
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().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.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
|
// mimeTypeToCategories maps MIME types to desktop file categories
|
||||||
|
|||||||
414
core/cmd/dms/commands_screenshot.go
Normal file
414
core/cmd/dms/commands_screenshot.go
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
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/DWL)
|
||||||
|
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. Supported on Hyprland and DWL.`,
|
||||||
|
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)
|
||||||
|
|
||||||
|
var swapRB bool
|
||||||
|
switch pixelFormat {
|
||||||
|
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
|
||||||
|
swapRB = false
|
||||||
|
default:
|
||||||
|
swapRB = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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+3 >= len(data) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
|
||||||
|
if o.FractionalScale == float64(int(o.FractionalScale)) {
|
||||||
|
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
|
||||||
|
}
|
||||||
|
|
||||||
|
transformStr := transformName(o.Transform)
|
||||||
|
|
||||||
|
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
|
||||||
|
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformName(t int32) string {
|
||||||
|
switch t {
|
||||||
|
case 0:
|
||||||
|
return "normal"
|
||||||
|
case 1:
|
||||||
|
return "90"
|
||||||
|
case 2:
|
||||||
|
return "180"
|
||||||
|
case 3:
|
||||||
|
return "270"
|
||||||
|
case 4:
|
||||||
|
return "flipped"
|
||||||
|
case 5:
|
||||||
|
return "flipped-90"
|
||||||
|
case 6:
|
||||||
|
return "flipped-180"
|
||||||
|
case 7:
|
||||||
|
return "flipped-270"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ func runSetup() error {
|
|||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
|
useSystemd := promptSystemd()
|
||||||
|
|
||||||
if !wmSelected && !terminalSelected {
|
if !wmSelected && !terminalSelected {
|
||||||
fmt.Println("No configurations selected. Exiting.")
|
fmt.Println("No configurations selected. Exiting.")
|
||||||
@@ -67,14 +68,14 @@ func runSetup() error {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if wmSelected && terminalSelected {
|
if wmSelected && terminalSelected {
|
||||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal)
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
|
||||||
} else if wmSelected {
|
} else if wmSelected {
|
||||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
|
||||||
if len(results) > 1 {
|
if len(results) > 1 {
|
||||||
results = results[:1]
|
results = results[:1]
|
||||||
}
|
}
|
||||||
} else if terminalSelected {
|
} else if terminalSelected {
|
||||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
|
||||||
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
||||||
results = results[1:]
|
results = results[1:]
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,19 @@ func promptTerminal() (deps.Terminal, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptSystemd() bool {
|
||||||
|
fmt.Println("\nUse systemd for session management?")
|
||||||
|
fmt.Println("1) Yes (recommended for most distros)")
|
||||||
|
fmt.Println("2) No (standalone, no systemd integration)")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Print("\nChoice (1-2): ")
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
|
return response != "2"
|
||||||
|
}
|
||||||
|
|
||||||
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
|
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
|
||||||
homeDir := os.Getenv("HOME")
|
homeDir := os.Getenv("HOME")
|
||||||
willBackup := false
|
willBackup := false
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func init() {
|
|||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func init() {
|
|||||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ipcTargets map[string]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() {
|
||||||
@@ -102,7 +104,6 @@ func getAllDMSPIDs() []int {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the child process is still alive
|
|
||||||
proc, err := os.FindProcess(childPID)
|
proc, err := os.FindProcess(childPID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Remove(pidFile)
|
os.Remove(pidFile)
|
||||||
@@ -110,18 +111,15 @@ func getAllDMSPIDs() []int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||||
// Process is dead, remove stale PID file
|
|
||||||
os.Remove(pidFile)
|
os.Remove(pidFile)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pids = append(pids, childPID)
|
pids = append(pids, childPID)
|
||||||
|
|
||||||
// Also get the parent PID from the filename
|
|
||||||
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
||||||
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
||||||
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
||||||
// Check if parent is still alive
|
|
||||||
if parentProc, err := os.FindProcess(parentPID); err == nil {
|
if parentProc, err := os.FindProcess(parentPID); err == nil {
|
||||||
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
|
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
|
||||||
pids = append(pids, parentPID)
|
pids = append(pids, parentPID)
|
||||||
@@ -144,7 +142,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)
|
||||||
@@ -157,6 +155,7 @@ func runShellInteractive(session bool) {
|
|||||||
errChan <- fmt.Errorf("server panic: %v", r)
|
errChan <- fmt.Errorf("server panic: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
server.CLIVersion = Version
|
||||||
if err := server.Start(false); err != nil {
|
if err := server.Start(false); err != nil {
|
||||||
errChan <- fmt.Errorf("server error: %w", err)
|
errChan <- fmt.Errorf("server error: %w", err)
|
||||||
}
|
}
|
||||||
@@ -223,7 +222,6 @@ func runShellInteractive(session bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// All other signals: clean shutdown
|
|
||||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
log.Infof("\nReceived signal %v, shutting down...", sig)
|
||||||
cancel()
|
cancel()
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
@@ -280,7 +278,6 @@ func restartShell() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func killShell() {
|
func killShell() {
|
||||||
// Get all tracked DMS PIDs from PID files
|
|
||||||
pids := getAllDMSPIDs()
|
pids := getAllDMSPIDs()
|
||||||
|
|
||||||
if len(pids) == 0 {
|
if len(pids) == 0 {
|
||||||
@@ -291,14 +288,12 @@ func killShell() {
|
|||||||
currentPid := os.Getpid()
|
currentPid := os.Getpid()
|
||||||
uniquePids := make(map[int]bool)
|
uniquePids := make(map[int]bool)
|
||||||
|
|
||||||
// Deduplicate and filter out current process
|
|
||||||
for _, pid := range pids {
|
for _, pid := range pids {
|
||||||
if pid != currentPid {
|
if pid != currentPid {
|
||||||
uniquePids[pid] = true
|
uniquePids[pid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill all tracked processes
|
|
||||||
for pid := range uniquePids {
|
for pid := range uniquePids {
|
||||||
proc, err := os.FindProcess(pid)
|
proc, err := os.FindProcess(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -306,7 +301,6 @@ func killShell() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if process is still alive before killing
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -318,7 +312,6 @@ func killShell() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any remaining PID files
|
|
||||||
dir := getRuntimeDir()
|
dir := getRuntimeDir()
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -335,7 +328,6 @@ func killShell() {
|
|||||||
|
|
||||||
func runShellDaemon(session bool) {
|
func runShellDaemon(session bool) {
|
||||||
isSessionManaged = session
|
isSessionManaged = session
|
||||||
// Check if this is the daemon child process by looking for the hidden flag
|
|
||||||
isDaemonChild := false
|
isDaemonChild := false
|
||||||
for _, arg := range os.Args {
|
for _, arg := range os.Args {
|
||||||
if arg == "--daemon-child" {
|
if arg == "--daemon-child" {
|
||||||
@@ -370,7 +362,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 +465,79 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||||
|
targets := make(ipcTargets)
|
||||||
|
var currentTarget string
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
if strings.HasPrefix(line, "target ") {
|
||||||
|
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
||||||
|
targets[currentTarget] = make(map[string][]string)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
||||||
|
argsList := []string{}
|
||||||
|
currentFunc := strings.TrimPrefix(line, " function ")
|
||||||
|
funcDef := strings.SplitN(currentFunc, "(", 2)
|
||||||
|
argList := strings.SplitN(funcDef[1], ")", 2)[0]
|
||||||
|
args := strings.Split(argList, ",")
|
||||||
|
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
|
||||||
|
argsList = append(argsList, funcDef[0])
|
||||||
|
for _, arg := range args {
|
||||||
|
argName := strings.SplitN(strings.TrimSpace(arg), ":", 2)[0]
|
||||||
|
argsList = append(argsList, argName)
|
||||||
|
}
|
||||||
|
targets[currentTarget][funcDef[0]] = argsList
|
||||||
|
} else {
|
||||||
|
targets[currentTarget][funcDef[0]] = make([]string, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|
||||||
|
func getShellIPCCompletions(args []string, _ string) []string {
|
||||||
|
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
||||||
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
var targets ipcTargets
|
||||||
|
|
||||||
|
if output, err := cmd.Output(); err == nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if len(args) == 1 {
|
||||||
|
if targetFuncs, ok := targets[args[0]]; ok {
|
||||||
|
funcNames := make([]string, 0)
|
||||||
|
for k := range targetFuncs {
|
||||||
|
funcNames = append(funcNames, k)
|
||||||
|
}
|
||||||
|
return funcNames
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(args) <= len(targets[args[0]]) {
|
||||||
|
funcArgs := targets[args[0]][args[1]]
|
||||||
|
if len(funcArgs) >= len(args) {
|
||||||
|
return []string{fmt.Sprintf("[%s]", funcArgs[len(args)-1])}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commandExists(cmd string) bool {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// findCommandPath returns the absolute path to a command in PATH
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
func findCommandPath(cmd string) (string, error) {
|
||||||
path, err := exec.LookPath(cmd)
|
path, err := exec.LookPath(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
750
core/internal/colorpicker/picker.go
Normal file
750
core/internal/colorpicker/picker.go
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
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 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
|
||||||
|
transform int32
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra roundtrip to ensure pointer/keyboard from seat capabilities are registered
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip after seat: %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
|
||||||
|
o.transform = int32(e.Transform)
|
||||||
|
}
|
||||||
|
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.scale <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return out.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()
|
||||||
|
|
||||||
|
screenBuf := ls.state.ScreenBuffer()
|
||||||
|
if screenBuf != nil && ls.output.transform != TransformNormal {
|
||||||
|
invTransform := InverseTransform(ls.output.transform)
|
||||||
|
transformed, err := screenBuf.ApplyTransform(invTransform)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("apply transform failed", "err", err)
|
||||||
|
} else if transformed != screenBuf {
|
||||||
|
ls.state.ReplaceScreenBuffer(transformed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logicalW, _ := ls.state.LogicalSize()
|
||||||
|
screenBuf = ls.state.ScreenBuffer()
|
||||||
|
if logicalW > 0 && screenBuf != nil {
|
||||||
|
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.viewport != nil {
|
||||||
|
_ = ls.wlSurface.SetBufferScale(1)
|
||||||
|
_ = ls.viewport.SetSource(0, 0, float64(renderBuf.Width), float64(renderBuf.Height))
|
||||||
|
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
|
||||||
|
} else {
|
||||||
|
bufferScale := ls.output.scale
|
||||||
|
if bufferScale <= 0 {
|
||||||
|
bufferScale = 1
|
||||||
|
}
|
||||||
|
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||||
|
}
|
||||||
|
_ = 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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.pointer = pointer
|
||||||
|
p.setupPointerHandlers()
|
||||||
|
}
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
|
||||||
|
keyboard, err := p.seat.GetKeyboard()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
55
core/internal/colorpicker/shm.go
Normal file
55
core/internal/colorpicker/shm.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
|
|
||||||
|
type ShmBuffer = shm.Buffer
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransformNormal = shm.TransformNormal
|
||||||
|
Transform90 = shm.Transform90
|
||||||
|
Transform180 = shm.Transform180
|
||||||
|
Transform270 = shm.Transform270
|
||||||
|
TransformFlipped = shm.TransformFlipped
|
||||||
|
TransformFlipped90 = shm.TransformFlipped90
|
||||||
|
TransformFlipped180 = shm.TransformFlipped180
|
||||||
|
TransformFlipped270 = shm.TransformFlipped270
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
|
return shm.CreateBuffer(width, height, stride)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InverseTransform(transform int32) int32 {
|
||||||
|
return shm.InverseTransform(transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
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],
|
||||||
|
}
|
||||||
|
}
|
||||||
1189
core/internal/colorpicker/state.go
Normal file
1189
core/internal/colorpicker/state.go
Normal file
File diff suppressed because it is too large
Load Diff
314
core/internal/colorpicker/state_test.go
Normal file
314
core/internal/colorpicker/state_test.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 50
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 30
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := 0; i < goroutines/2; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
s.SetScale(int32(id%3 + 1))
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < goroutines/2; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
scale := s.Scale()
|
||||||
|
assert.GreaterOrEqual(t, scale, int32(1))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 20
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := 0; i < goroutines/2; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < goroutines/2; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
w, h := s.LogicalSize()
|
||||||
|
_ = w
|
||||||
|
_ = h
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 30
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := 0; i < goroutines/3; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
s.OnPointerButton(0x110, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < goroutines/3; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
s.OnKey(1, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < goroutines/3; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
picked, cancelled := s.IsDone()
|
||||||
|
_ = picked
|
||||||
|
_ = cancelled
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 20
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
_ = s.IsReady()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 20
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < iterations; j++ {
|
||||||
|
s.SwapBuffers()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ZeroScale(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
s.SetScale(0)
|
||||||
|
assert.Equal(t, int32(1), s.Scale())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_NegativeScale(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
s.SetScale(-5)
|
||||||
|
assert.Equal(t, int32(1), s.Scale())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ZeroDimensionConfigure(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
err := s.OnLayerConfigure(0, 100)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.OnLayerConfigure(100, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.OnLayerConfigure(-1, 100)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
w, h := s.LogicalSize()
|
||||||
|
assert.Equal(t, 0, w)
|
||||||
|
assert.Equal(t, 0, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
color, ok := s.PickColor()
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Equal(t, Color{}, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.Redraw()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.RedrawScreenOnly()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_FrontRenderBufferNil(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.FrontRenderBuffer()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ScreenBufferNil(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.ScreenBuffer()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_DestroyMultipleTimes(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
s.Destroy()
|
||||||
|
s.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClamp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v, lo, hi, expected int
|
||||||
|
}{
|
||||||
|
{5, 0, 10, 5},
|
||||||
|
{-5, 0, 10, 0},
|
||||||
|
{15, 0, 10, 10},
|
||||||
|
{0, 0, 10, 0},
|
||||||
|
{10, 0, 10, 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := clamp(tt.v, tt.lo, tt.hi)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClampF(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v, lo, hi, expected float64
|
||||||
|
}{
|
||||||
|
{5.0, 0.0, 10.0, 5.0},
|
||||||
|
{-5.0, 0.0, 10.0, 0.0},
|
||||||
|
{15.0, 0.0, 10.0, 10.0},
|
||||||
|
{0.0, 0.0, 10.0, 0.0},
|
||||||
|
{10.0, 0.0, 10.0, 10.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := clampF(tt.v, tt.lo, tt.hi)
|
||||||
|
assert.InDelta(t, tt.expected, result, 0.001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v, expected int
|
||||||
|
}{
|
||||||
|
{5, 5},
|
||||||
|
{-5, 5},
|
||||||
|
{0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := abs(tt.v)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlendColors(t *testing.T) {
|
||||||
|
bg := Color{R: 0, G: 0, B: 0, A: 255}
|
||||||
|
fg := Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
|
||||||
|
result := blendColors(bg, fg, 0.0)
|
||||||
|
assert.Equal(t, bg.R, result.R)
|
||||||
|
assert.Equal(t, bg.G, result.G)
|
||||||
|
assert.Equal(t, bg.B, result.B)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, 1.0)
|
||||||
|
assert.Equal(t, fg.R, result.R)
|
||||||
|
assert.Equal(t, fg.G, result.G)
|
||||||
|
assert.Equal(t, fg.B, result.B)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, 0.5)
|
||||||
|
assert.InDelta(t, 127, int(result.R), 1)
|
||||||
|
assert.InDelta(t, 127, int(result.G), 1)
|
||||||
|
assert.InDelta(t, 127, int(result.B), 1)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, -1.0)
|
||||||
|
assert.Equal(t, bg.R, result.R)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, 2.0)
|
||||||
|
assert.Equal(t, fg.R, result.R)
|
||||||
|
}
|
||||||
@@ -46,11 +46,20 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
|
|||||||
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
|
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeployConfigurationsWithSystemd deploys configurations with systemd option
|
||||||
|
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
|
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
|
||||||
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
|
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
|
||||||
|
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
shouldReplaceConfig := func(configType string) bool {
|
shouldReplaceConfig := func(configType string) bool {
|
||||||
@@ -64,7 +73,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
if shouldReplaceConfig("Niri") {
|
if shouldReplaceConfig("Niri") {
|
||||||
result, err := cd.deployNiriConfig(terminal)
|
result, err := cd.deployNiriConfig(terminal, useSystemd)
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
|
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
|
||||||
@@ -72,7 +81,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
}
|
}
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
if shouldReplaceConfig("Hyprland") {
|
if shouldReplaceConfig("Hyprland") {
|
||||||
result, err := cd.deployHyprlandConfig(terminal)
|
result, err := cd.deployHyprlandConfig(terminal, useSystemd)
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
||||||
@@ -110,8 +119,7 @@ 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, useSystemd bool) (DeploymentResult, error) {
|
||||||
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Niri",
|
ConfigType: "Niri",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||||
@@ -123,6 +131,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 +157,6 @@ 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()
|
|
||||||
if err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
|
||||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine terminal command based on choice
|
|
||||||
var terminalCommand string
|
var terminalCommand string
|
||||||
switch terminal {
|
switch terminal {
|
||||||
case deps.TerminalGhostty:
|
case deps.TerminalGhostty:
|
||||||
@@ -160,13 +166,15 @@ 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, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
|
||||||
|
if !useSystemd {
|
||||||
|
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, 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 +190,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
|
||||||
|
|
||||||
@@ -375,41 +410,6 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectPolkitAgent tries to find the polkit authentication agent on the system
|
|
||||||
// Prioritizes mate-polkit paths since that's what we install
|
|
||||||
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
|
|
||||||
// Prioritize mate-polkit paths first
|
|
||||||
matePaths := []string{
|
|
||||||
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
|
|
||||||
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
|
|
||||||
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
|
|
||||||
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
|
|
||||||
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range matePaths {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to other polkit agents if mate-polkit is not found
|
|
||||||
fallbackPaths := []string{
|
|
||||||
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
|
||||||
"/usr/libexec/polkit-gnome-authentication-agent-1",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range fallbackPaths {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("no polkit agent found in common locations")
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
|
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
|
||||||
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
|
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
|
||||||
// Regular expression to match output sections (including commented ones)
|
// Regular expression to match output sections (including commented ones)
|
||||||
@@ -453,7 +453,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
|
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
|
||||||
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Hyprland",
|
ConfigType: "Hyprland",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||||
@@ -485,14 +485,6 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
|
|||||||
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()
|
|
||||||
if err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
|
||||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine terminal command based on choice
|
|
||||||
var terminalCommand string
|
var terminalCommand string
|
||||||
switch terminal {
|
switch terminal {
|
||||||
case deps.TerminalGhostty:
|
case deps.TerminalGhostty:
|
||||||
@@ -502,13 +494,15 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
|
|||||||
case deps.TerminalAlacritty:
|
case deps.TerminalAlacritty:
|
||||||
terminalCommand = "alacritty"
|
terminalCommand = "alacritty"
|
||||||
default:
|
default:
|
||||||
terminalCommand = "ghostty" // fallback to ghostty
|
terminalCommand = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
|
||||||
|
if !useSystemd {
|
||||||
|
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
|
||||||
|
}
|
||||||
|
|
||||||
// If there was an existing config, merge the monitor sections
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
|
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -531,24 +525,16 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
|
|||||||
|
|
||||||
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
|
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
|
||||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
|
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
|
||||||
// Regular expression to match monitor lines (including commented ones)
|
|
||||||
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
|
|
||||||
// Also matches commented versions: # monitor = ...
|
|
||||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||||
|
|
||||||
// Find all monitor lines in the existing config
|
|
||||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||||
|
|
||||||
if len(existingMonitors) == 0 {
|
if len(existingMonitors) == 0 {
|
||||||
// No monitor sections to merge
|
|
||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the example monitor line from the new config
|
|
||||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
||||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
||||||
|
|
||||||
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
|
|
||||||
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
||||||
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
||||||
|
|
||||||
@@ -556,8 +542,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
|||||||
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert after the header
|
insertPos := headerMatch[1] + 1
|
||||||
insertPos := headerMatch[1] + 1 // +1 for the newline
|
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString(mergedConfig[:insertPos])
|
builder.WriteString(mergedConfig[:insertPos])
|
||||||
@@ -572,3 +557,69 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
|||||||
|
|
||||||
return builder.String(), nil
|
return builder.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
|
lines := strings.Split(config, "\n")
|
||||||
|
var result []string
|
||||||
|
startupSectionFound := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
||||||
|
startupSectionFound = true
|
||||||
|
result = append(result, "exec-once = dms run")
|
||||||
|
result = append(result, "env = QT_QPA_PLATFORM,wayland")
|
||||||
|
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
||||||
|
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
||||||
|
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
||||||
|
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !startupSectionFound {
|
||||||
|
for i, line := range result {
|
||||||
|
if strings.Contains(line, "STARTUP APPS") {
|
||||||
|
insertLines := []string{
|
||||||
|
"exec-once = dms run",
|
||||||
|
"env = QT_QPA_PLATFORM,wayland",
|
||||||
|
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
||||||
|
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
||||||
|
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
||||||
|
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
|
||||||
|
}
|
||||||
|
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
|
envVars := fmt.Sprintf(`environment {
|
||||||
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
|
QT_QPA_PLATFORM "wayland"
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||||
|
QT_QPA_PLATFORMTHEME "gtk3"
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
||||||
|
TERMINAL "%s"
|
||||||
|
}`, terminalCommand)
|
||||||
|
|
||||||
|
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
|
||||||
|
|
||||||
|
spawnDms := `spawn-at-startup "dms" "run"`
|
||||||
|
if !strings.Contains(config, spawnDms) {
|
||||||
|
config = strings.Replace(config,
|
||||||
|
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`,
|
||||||
|
`spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"`+"\n"+spawnDms,
|
||||||
|
1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -11,23 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDetectPolkitAgent(t *testing.T) {
|
|
||||||
cd := &ConfigDeployer{}
|
|
||||||
|
|
||||||
// This test depends on the system having a polkit agent installed
|
|
||||||
// We'll just test that the function doesn't crash and returns some path or error
|
|
||||||
path, err := cd.detectPolkitAgent()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// If no polkit agent is found, that's okay for testing
|
|
||||||
assert.Contains(t, err.Error(), "no polkit agent found")
|
|
||||||
} else {
|
|
||||||
// If found, it should be a valid path
|
|
||||||
assert.NotEmpty(t, path)
|
|
||||||
assert.True(t, strings.Contains(path, "polkit"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergeNiriOutputSections(t *testing.T) {
|
func TestMergeNiriOutputSections(t *testing.T) {
|
||||||
cd := &ConfigDeployer{}
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
@@ -272,17 +254,6 @@ func getGhosttyPath() string {
|
|||||||
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolkitPathInjection(t *testing.T) {
|
|
||||||
|
|
||||||
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
|
||||||
other content`
|
|
||||||
|
|
||||||
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
|
|
||||||
|
|
||||||
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
|
|
||||||
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||||
cd := &ConfigDeployer{}
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
@@ -424,7 +395,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
cd := NewConfigDeployer(logChan)
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
|
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||||
@@ -454,7 +425,7 @@ general {
|
|||||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||||
@@ -479,21 +450,17 @@ 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, NiriBindsConfig, "binds {")
|
||||||
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandConfigStructure(t *testing.T) {
|
func TestHyprlandConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
|
|
||||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||||
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
|
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, {{TERMINAL_COMMAND}}")
|
||||||
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}")
|
|
||||||
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||||
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
|
|
||||||
func LocateDMSConfig() (string, error) {
|
func LocateDMSConfig() (string, error) {
|
||||||
var primaryPaths []string
|
var primaryPaths []string
|
||||||
|
|
||||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
configHome := utils.XDGConfigHome()
|
||||||
if configHome == "" {
|
|
||||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
|
||||||
configHome = filepath.Join(homeDir, ".config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if configHome != "" {
|
if configHome != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,12 @@
|
|||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||||
monitor = , preferred,auto,auto
|
monitor = , preferred,auto,auto
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ENVIRONMENT VARS
|
|
||||||
# ==================
|
|
||||||
env = QT_QPA_PLATFORM,wayland
|
|
||||||
env = ELECTRON_OZONE_PLATFORM_HINT,auto
|
|
||||||
env = QT_QPA_PLATFORMTHEME,gtk3
|
|
||||||
env = QT_QPA_PLATFORMTHEME_QT6,gtk3
|
|
||||||
env = TERMINAL,{{TERMINAL_COMMAND}}
|
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# STARTUP APPS
|
# STARTUP APPS
|
||||||
# ==================
|
# ==================
|
||||||
|
exec-once = dbus-update-activation-environment --systemd --all
|
||||||
|
exec-once = systemctl --user start hyprland-session.target
|
||||||
exec-once = bash -c "wl-paste --watch cliphist store &"
|
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||||
exec-once = dms run
|
|
||||||
exec-once = {{POLKIT_AGENT_PATH}}
|
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# INPUT CONFIG
|
# INPUT CONFIG
|
||||||
@@ -140,8 +131,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 +144,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
|
||||||
@@ -281,12 +272,9 @@ binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
|||||||
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
||||||
|
|
||||||
# === Screenshots ===
|
# === Screenshots ===
|
||||||
bind = , XF86Launch1, exec, grimblast copy area
|
bind = , Print, exec, dms screenshot
|
||||||
bind = CTRL, XF86Launch1, exec, grimblast copy screen
|
bind = CTRL, Print, exec, dms screenshot full
|
||||||
bind = ALT, XF86Launch1, exec, grimblast copy active
|
bind = ALT, Print, exec, dms screenshot window
|
||||||
bind = , Print, exec, grimblast copy area
|
|
||||||
bind = CTRL, Print, exec, grimblast copy screen
|
|
||||||
bind = ALT, Print, exec, grimblast copy active
|
|
||||||
|
|
||||||
# === System Controls ===
|
# === System Controls ===
|
||||||
bind = $mod SHIFT, P, dpms, off
|
bind = $mod SHIFT, P, dpms, toggle
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -44,7 +44,6 @@ input {
|
|||||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
||||||
layout {
|
layout {
|
||||||
// Set gaps around windows in logical pixels.
|
// Set gaps around windows in logical pixels.
|
||||||
gaps 5
|
|
||||||
background-color "transparent"
|
background-color "transparent"
|
||||||
// When to center a column when changing focus, options are:
|
// When to center a column when changing focus, options are:
|
||||||
// - "never", default behavior, focusing an off-screen column will keep at the left
|
// - "never", default behavior, focusing an off-screen column will keep at the left
|
||||||
@@ -87,11 +86,6 @@ layout {
|
|||||||
inactive-color "#d0d0d0" // Light gray
|
inactive-color "#d0d0d0" // Light gray
|
||||||
urgent-color "#cc4444" // Softer red
|
urgent-color "#cc4444" // Softer red
|
||||||
}
|
}
|
||||||
focus-ring {
|
|
||||||
width 2
|
|
||||||
active-color "#808080" // Medium gray
|
|
||||||
inactive-color "#505050" // Dark gray
|
|
||||||
}
|
|
||||||
shadow {
|
shadow {
|
||||||
softness 30
|
softness 30
|
||||||
spread 5
|
spread 5
|
||||||
@@ -116,15 +110,8 @@ overview {
|
|||||||
// See the binds section below for more spawn examples.
|
// See the binds section below for more spawn examples.
|
||||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
||||||
spawn-at-startup "dms" "run"
|
|
||||||
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
|
||||||
environment {
|
environment {
|
||||||
XDG_CURRENT_DESKTOP "niri"
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
QT_QPA_PLATFORM "wayland"
|
|
||||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
|
||||||
QT_QPA_PLATFORMTHEME "gtk3"
|
|
||||||
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
|
||||||
TERMINAL "{{TERMINAL_COMMAND}}"
|
|
||||||
}
|
}
|
||||||
hotkey-overlay {
|
hotkey-overlay {
|
||||||
skip-at-startup
|
skip-at-startup
|
||||||
@@ -214,210 +201,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
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ type ColorInfo struct {
|
|||||||
B int `json:"b"`
|
B int `json:"b"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VariantColorValue struct {
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
HexStripped string `json:"hex_stripped"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VariantColorInfo struct {
|
||||||
|
Dark VariantColorValue `json:"dark"`
|
||||||
|
Light VariantColorValue `json:"light"`
|
||||||
|
Default VariantColorValue `json:"default"`
|
||||||
|
}
|
||||||
|
|
||||||
type Palette struct {
|
type Palette struct {
|
||||||
Color0 ColorInfo `json:"color0"`
|
Color0 ColorInfo `json:"color0"`
|
||||||
Color1 ColorInfo `json:"color1"`
|
Color1 ColorInfo `json:"color1"`
|
||||||
@@ -42,6 +53,25 @@ type Palette struct {
|
|||||||
Color15 ColorInfo `json:"color15"`
|
Color15 ColorInfo `json:"color15"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VariantPalette struct {
|
||||||
|
Color0 VariantColorInfo `json:"color0"`
|
||||||
|
Color1 VariantColorInfo `json:"color1"`
|
||||||
|
Color2 VariantColorInfo `json:"color2"`
|
||||||
|
Color3 VariantColorInfo `json:"color3"`
|
||||||
|
Color4 VariantColorInfo `json:"color4"`
|
||||||
|
Color5 VariantColorInfo `json:"color5"`
|
||||||
|
Color6 VariantColorInfo `json:"color6"`
|
||||||
|
Color7 VariantColorInfo `json:"color7"`
|
||||||
|
Color8 VariantColorInfo `json:"color8"`
|
||||||
|
Color9 VariantColorInfo `json:"color9"`
|
||||||
|
Color10 VariantColorInfo `json:"color10"`
|
||||||
|
Color11 VariantColorInfo `json:"color11"`
|
||||||
|
Color12 VariantColorInfo `json:"color12"`
|
||||||
|
Color13 VariantColorInfo `json:"color13"`
|
||||||
|
Color14 VariantColorInfo `json:"color14"`
|
||||||
|
Color15 VariantColorInfo `json:"color15"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewColorInfo(hex string) ColorInfo {
|
func NewColorInfo(hex string) ColorInfo {
|
||||||
rgb := HexToRGB(hex)
|
rgb := HexToRGB(hex)
|
||||||
stripped := hex
|
stripped := hex
|
||||||
@@ -83,13 +113,14 @@ func RGBToHSV(rgb RGB) HSV {
|
|||||||
delta := max - min
|
delta := max - min
|
||||||
|
|
||||||
var h float64
|
var h float64
|
||||||
if delta == 0 {
|
switch {
|
||||||
|
case delta == 0:
|
||||||
h = 0
|
h = 0
|
||||||
} else if max == rgb.R {
|
case max == rgb.R:
|
||||||
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
|
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
|
||||||
} else if max == rgb.G {
|
case max == rgb.G:
|
||||||
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
|
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
|
||||||
} else {
|
default:
|
||||||
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
|
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,3 +523,54 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
|
|
||||||
return palette
|
return palette
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VariantOptions struct {
|
||||||
|
PrimaryDark string
|
||||||
|
PrimaryLight string
|
||||||
|
Background string
|
||||||
|
UseDPS bool
|
||||||
|
IsLightMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
|
||||||
|
darkVal := VariantColorValue{Hex: dark.Hex, HexStripped: dark.HexStripped}
|
||||||
|
lightVal := VariantColorValue{Hex: light.Hex, HexStripped: light.HexStripped}
|
||||||
|
|
||||||
|
defaultVal := darkVal
|
||||||
|
if isLightMode {
|
||||||
|
defaultVal = lightVal
|
||||||
|
}
|
||||||
|
|
||||||
|
return VariantColorInfo{
|
||||||
|
Dark: darkVal,
|
||||||
|
Light: lightVal,
|
||||||
|
Default: defaultVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateVariantPalette(opts VariantOptions) VariantPalette {
|
||||||
|
darkOpts := PaletteOptions{IsLight: false, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||||
|
lightOpts := PaletteOptions{IsLight: true, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||||
|
|
||||||
|
dark := GeneratePalette(opts.PrimaryDark, darkOpts)
|
||||||
|
light := GeneratePalette(opts.PrimaryLight, lightOpts)
|
||||||
|
|
||||||
|
return VariantPalette{
|
||||||
|
Color0: mergeColorInfo(dark.Color0, light.Color0, opts.IsLightMode),
|
||||||
|
Color1: mergeColorInfo(dark.Color1, light.Color1, opts.IsLightMode),
|
||||||
|
Color2: mergeColorInfo(dark.Color2, light.Color2, opts.IsLightMode),
|
||||||
|
Color3: mergeColorInfo(dark.Color3, light.Color3, opts.IsLightMode),
|
||||||
|
Color4: mergeColorInfo(dark.Color4, light.Color4, opts.IsLightMode),
|
||||||
|
Color5: mergeColorInfo(dark.Color5, light.Color5, opts.IsLightMode),
|
||||||
|
Color6: mergeColorInfo(dark.Color6, light.Color6, opts.IsLightMode),
|
||||||
|
Color7: mergeColorInfo(dark.Color7, light.Color7, opts.IsLightMode),
|
||||||
|
Color8: mergeColorInfo(dark.Color8, light.Color8, opts.IsLightMode),
|
||||||
|
Color9: mergeColorInfo(dark.Color9, light.Color9, opts.IsLightMode),
|
||||||
|
Color10: mergeColorInfo(dark.Color10, light.Color10, opts.IsLightMode),
|
||||||
|
Color11: mergeColorInfo(dark.Color11, light.Color11, opts.IsLightMode),
|
||||||
|
Color12: mergeColorInfo(dark.Color12, light.Color12, opts.IsLightMode),
|
||||||
|
Color13: mergeColorInfo(dark.Color13, light.Color13, opts.IsLightMode),
|
||||||
|
Color14: mergeColorInfo(dark.Color14, light.Color14, opts.IsLightMode),
|
||||||
|
Color15: mergeColorInfo(dark.Color15, light.Color15, opts.IsLightMode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ func GenerateJSON(p Palette) string {
|
|||||||
return string(marshalled)
|
return string(marshalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateVariantJSON(p VariantPalette) string {
|
||||||
|
marshalled, _ := json.Marshal(p)
|
||||||
|
return string(marshalled)
|
||||||
|
}
|
||||||
|
|
||||||
func GenerateKittyTheme(p Palette) string {
|
func GenerateKittyTheme(p Palette) string {
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
|
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, a.detectQuickshell())
|
dependencies = append(dependencies, a.detectQuickshell())
|
||||||
dependencies = append(dependencies, a.detectXDGPortal())
|
dependencies = append(dependencies, a.detectXDGPortal())
|
||||||
dependencies = append(dependencies, a.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, a.detectAccountsService())
|
dependencies = append(dependencies, a.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -107,52 +106,17 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
dependencies = append(dependencies, a.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, a.detectClipboardTools()...)
|
dependencies = append(dependencies, a.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if a.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if a.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
||||||
if a.packageInstalled("accountsservice") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "accountsservice",
|
|
||||||
Status: status,
|
|
||||||
Description: "D-Bus interface for user account query and manipulation",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -178,18 +142,13 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"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},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
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"] = a.getNiriMapping(variants["niri"])
|
packages["niri"] = a.getNiriMapping(variants["niri"])
|
||||||
@@ -203,13 +162,11 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
|
|||||||
if forceQuickshellGit || variant == deps.VariantGit {
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
|
// ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +335,19 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := a.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := a.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.EnableDMSService(ctx, wm); err != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 7: Complete
|
// Phase 7: Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
|
|||||||
@@ -14,11 +14,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const forceQuickshellGit = false
|
const (
|
||||||
const forceDMSGit = false
|
forceQuickshellGit = false
|
||||||
|
forceDMSGit = false
|
||||||
|
)
|
||||||
|
|
||||||
// BaseDistribution provides common functionality for all distributions
|
// BaseDistribution provides common functionality for all distributions
|
||||||
type BaseDistribution struct {
|
type BaseDistribution struct {
|
||||||
@@ -74,47 +77,42 @@ func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *
|
|||||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common dependency detection methods
|
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if b.commandExists("git") {
|
if b.commandExists(name) {
|
||||||
status = deps.StatusInstalled
|
status = deps.StatusInstalled
|
||||||
}
|
}
|
||||||
|
|
||||||
return deps.Dependency{
|
return deps.Dependency{
|
||||||
Name: "git",
|
Name: name,
|
||||||
Status: status,
|
Status: status,
|
||||||
Description: "Version control system",
|
Description: description,
|
||||||
Required: true,
|
Required: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectPackage(name, description string, installed bool) deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if installed {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: name,
|
||||||
|
Status: status,
|
||||||
|
Description: description,
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||||
|
return b.detectCommand("git", "Version control system")
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return b.detectCommand("matugen", "Material Design color generation tool")
|
||||||
if b.commandExists("matugen") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "matugen",
|
|
||||||
Status: status,
|
|
||||||
Description: "Material Design color generation tool",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return b.detectCommand("dgop", "Desktop portal management tool")
|
||||||
if b.commandExists("dgop") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "dgop",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop portal management tool",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||||
@@ -219,20 +217,6 @@ func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
|||||||
return dependencies
|
return dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if b.commandExists("hyprpicker") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "hyprpicker",
|
|
||||||
Status: status,
|
|
||||||
Description: "Color picker for Wayland",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||||
var dependencies []deps.Dependency
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
@@ -240,10 +224,7 @@ func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
|||||||
name string
|
name string
|
||||||
description string
|
description string
|
||||||
}{
|
}{
|
||||||
{"grim", "Screenshot utility for Wayland"},
|
|
||||||
{"slurp", "Region selection utility for Wayland"},
|
|
||||||
{"hyprctl", "Hyprland control utility"},
|
{"hyprctl", "Hyprland control utility"},
|
||||||
{"grimblast", "Screenshot script for Hyprland"},
|
|
||||||
{"jq", "JSON processor"},
|
{"jq", "JSON processor"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,6 +545,117 @@ func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressCha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) DetectTerminalFromDeps(dependencies []deps.Dependency) deps.Terminal {
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
switch dep.Name {
|
||||||
|
case "ghostty":
|
||||||
|
return deps.TerminalGhostty
|
||||||
|
case "kitty":
|
||||||
|
return deps.TerminalKitty
|
||||||
|
case "alacritty":
|
||||||
|
return deps.TerminalAlacritty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps.TerminalGhostty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
||||||
|
if err := os.MkdirAll(envDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var terminalCmd string
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
terminalCmd = "ghostty"
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
terminalCmd = "kitty"
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
terminalCmd = "alacritty"
|
||||||
|
default:
|
||||||
|
terminalCmd = "ghostty"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ! This deviates from master branch so it doesnt need a hotfix
|
||||||
|
var content string
|
||||||
|
if utils.CommandExists("plasmashell") || utils.CommandExists("plasma-session") || utils.CommandExists("plasma_session") {
|
||||||
|
content = fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||||
|
TERMINAL=%s
|
||||||
|
`, terminalCmd)
|
||||||
|
} else {
|
||||||
|
content = fmt.Sprintf(`QT_QPA_PLATFORM=wayland
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||||
|
QT_QPA_PLATFORMTHEME=gtk3
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6=gtk3
|
||||||
|
TERMINAL=%s
|
||||||
|
`, terminalCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
envFile := filepath.Join(envDir, "90-dms.conf")
|
||||||
|
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write environment config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.log(fmt.Sprintf("Wrote environment config to %s", envFile))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
|
||||||
|
b.log("Warning: failed to add dms as a want for niri.service")
|
||||||
|
}
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil {
|
||||||
|
b.log("Warning: failed to add dms as a want for hyprland-session.target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error {
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
if err := b.WriteHyprlandSessionTarget(); err != nil {
|
||||||
|
return fmt.Errorf("failed to write hyprland session target: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(targetDir, "hyprland-session.target")
|
||||||
|
content := `[Unit]
|
||||||
|
Description=Hyprland Session Target
|
||||||
|
Requires=graphical-session.target
|
||||||
|
After=graphical-session.target
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// installDMSBinary installs the DMS binary from GitHub releases
|
// installDMSBinary installs the DMS binary from GitHub releases
|
||||||
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
b.log("Installing/updating DMS binary...")
|
b.log("Installing/updating DMS binary...")
|
||||||
@@ -602,7 +694,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
|||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
}
|
}
|
||||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
||||||
@@ -36,7 +37,7 @@ func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
||||||
if !commandExists("git") {
|
if !utils.CommandExists("git") {
|
||||||
t.Skip("git not available")
|
t.Skip("git not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
||||||
if !commandExists("git") {
|
if !utils.CommandExists("git") {
|
||||||
t.Skip("git not available")
|
t.Skip("git not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +165,6 @@ func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandExists(cmd string) bool {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBaseDistribution_versionCompare(t *testing.T) {
|
func TestBaseDistribution_versionCompare(t *testing.T) {
|
||||||
logChan := make(chan string, 10)
|
logChan := make(chan string, 10)
|
||||||
defer close(logChan)
|
defer close(logChan)
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, d.detectQuickshell())
|
dependencies = append(dependencies, d.detectQuickshell())
|
||||||
dependencies = append(dependencies, d.detectXDGPortal())
|
dependencies = append(dependencies, d.detectXDGPortal())
|
||||||
dependencies = append(dependencies, d.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, d.detectAccountsService())
|
dependencies = append(dependencies, d.detectAccountsService())
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
@@ -70,66 +69,21 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if d.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if d.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return d.detectCommand("xwayland-satellite", "Xwayland support")
|
||||||
if d.commandExists("xwayland-satellite") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xwayland-satellite",
|
|
||||||
Status: status,
|
|
||||||
Description: "Xwayland support",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
||||||
if d.packageInstalled("accountsservice") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "accountsservice",
|
|
||||||
Status: status,
|
|
||||||
Description: "D-Bus interface for user account query and manipulation",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -139,33 +93,65 @@ 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},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"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},
|
|
||||||
"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"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -192,7 +178,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -209,7 +195,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -238,8 +224,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 +255,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,
|
||||||
@@ -286,6 +303,19 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := d.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := d.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.EnableDMSService(ctx, wm); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
Progress: 1.0,
|
Progress: 1.0,
|
||||||
@@ -297,8 +327,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 +337,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 +357,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("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, 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("bash -c \"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 {
|
||||||
@@ -341,7 +476,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
args = append(args, packages...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -451,7 +586,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -490,34 +625,10 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
d.log("Installing Ghostty using Debian installer script...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Running Ghostty Debian installer...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
|
||||||
LogOutput: "Installing Ghostty using pre-built Debian package",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
|
||||||
|
|
||||||
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.log("Ghostty installed successfully using Debian installer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -527,10 +638,6 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "ghostty":
|
|
||||||
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, f.detectQuickshell())
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
dependencies = append(dependencies, f.detectXDGPortal())
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
dependencies = append(dependencies, f.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, f.detectAccountsService())
|
dependencies = append(dependencies, f.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -92,38 +91,13 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
dependencies = append(dependencies, f.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, f.detectClipboardTools()...)
|
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if f.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if f.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -145,9 +119,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"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},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
|
|
||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
@@ -160,10 +132,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
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"] = f.getNiriMapping(variants["niri"])
|
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||||
@@ -187,25 +156,15 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
|
|||||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
|
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
if variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||||
}
|
}
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -385,6 +344,19 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := f.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := f.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.EnableDMSService(ctx, wm); err != nil {
|
||||||
|
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 7: Complete
|
// Phase 7: Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, g.detectWindowManager(wm))
|
dependencies = append(dependencies, g.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, g.detectQuickshell())
|
dependencies = append(dependencies, g.detectQuickshell())
|
||||||
dependencies = append(dependencies, g.detectXDGPortal())
|
dependencies = append(dependencies, g.detectXDGPortal())
|
||||||
dependencies = append(dependencies, g.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, g.detectAccountsService())
|
dependencies = append(dependencies, g.detectAccountsService())
|
||||||
|
|
||||||
if wm == deps.WindowManagerHyprland {
|
if wm == deps.WindowManagerHyprland {
|
||||||
@@ -108,66 +107,21 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, g.detectMatugen())
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
dependencies = append(dependencies, g.detectDgop())
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
dependencies = append(dependencies, g.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, g.detectClipboardTools()...)
|
dependencies = append(dependencies, g.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
|
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return g.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", g.packageInstalled("sys-apps/xdg-desktop-portal-gtk"))
|
||||||
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if g.packageInstalled("mate-extra/mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite"))
|
||||||
if g.packageInstalled("gui-apps/xwayland-satellite") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xwayland-satellite",
|
|
||||||
Status: status,
|
|
||||||
Description: "Xwayland support",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
|
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice"))
|
||||||
if g.packageInstalled("sys-apps/accountsservice") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "accountsservice",
|
|
||||||
Status: status,
|
|
||||||
Description: "D-Bus interface for user account query and manipulation",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) packageInstalled(pkg string) bool {
|
func (g *GentooDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -188,9 +142,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||||
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
|
|
||||||
|
|
||||||
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
||||||
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||||
@@ -207,10 +159,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
|
||||||
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = g.getNiriMapping(variants["niri"])
|
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||||
@@ -228,16 +177,8 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
|
|||||||
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
|
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
archKeyword := g.getArchKeyword()
|
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()}
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
|
|
||||||
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
@@ -460,6 +401,19 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := g.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := g.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EnableDMSService(ctx, wm); err != nil {
|
||||||
|
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
Progress: 1.0,
|
Progress: 1.0,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install dgop: %w", err)
|
return fmt.Errorf("failed to install dgop: %w", err)
|
||||||
}
|
}
|
||||||
case "grimblast":
|
|
||||||
if err := m.installGrimblast(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
|
||||||
}
|
|
||||||
case "niri":
|
case "niri":
|
||||||
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install niri: %w", err)
|
return fmt.Errorf("failed to install niri: %w", err)
|
||||||
@@ -166,62 +162,6 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installGrimblast(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
m.log("Installing grimblast script for Hyprland...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Downloading grimblast script...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "curl grimblast script",
|
|
||||||
}
|
|
||||||
|
|
||||||
grimblastURL := "https://raw.githubusercontent.com/hyprwm/contrib/refs/heads/main/grimblast/grimblast"
|
|
||||||
tmpPath := filepath.Join(os.TempDir(), "grimblast")
|
|
||||||
|
|
||||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", "-o", tmpPath, grimblastURL)
|
|
||||||
if err := downloadCmd.Run(); err != nil {
|
|
||||||
m.logError("failed to download grimblast", err)
|
|
||||||
return fmt.Errorf("failed to download grimblast: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.5,
|
|
||||||
Step: "Making grimblast executable...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "chmod +x grimblast",
|
|
||||||
}
|
|
||||||
|
|
||||||
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", tmpPath)
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
m.logError("failed to make grimblast executable", err)
|
|
||||||
return fmt.Errorf("failed to make grimblast executable: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.8,
|
|
||||||
Step: "Installing grimblast to /usr/local/bin...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "sudo cp grimblast /usr/local/bin/",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
fmt.Sprintf("cp %s /usr/local/bin/grimblast", tmpPath))
|
|
||||||
if err := installCmd.Run(); err != nil {
|
|
||||||
m.logError("failed to install grimblast", err)
|
|
||||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
|
|
||||||
m.log("grimblast installed successfully to /usr/local/bin")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing niri from source...")
|
m.log("Installing niri from source...")
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, o.detectQuickshell())
|
dependencies = append(dependencies, o.detectQuickshell())
|
||||||
dependencies = append(dependencies, o.detectXDGPortal())
|
dependencies = append(dependencies, o.detectXDGPortal())
|
||||||
dependencies = append(dependencies, o.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, o.detectAccountsService())
|
dependencies = append(dependencies, o.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -82,38 +81,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if o.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if o.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -135,34 +109,59 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"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},
|
|
||||||
"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 {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
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 +293,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 +324,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 +353,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 +362,20 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: Complete
|
terminal := o.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := o.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.EnableDMSService(ctx, wm); err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
Progress: 1.0,
|
Progress: 1.0,
|
||||||
@@ -346,8 +387,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 +417,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 {
|
||||||
|
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", 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 {
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package distros
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -66,7 +64,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, u.detectQuickshell())
|
dependencies = append(dependencies, u.detectQuickshell())
|
||||||
dependencies = append(dependencies, u.detectXDGPortal())
|
dependencies = append(dependencies, u.detectXDGPortal())
|
||||||
dependencies = append(dependencies, u.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, u.detectAccountsService())
|
dependencies = append(dependencies, u.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -82,66 +79,21 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
|
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return u.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", u.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if u.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if u.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return u.detectCommand("xwayland-satellite", "Xwayland support")
|
||||||
if u.commandExists("xwayland-satellite") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xwayland-satellite",
|
|
||||||
Status: status,
|
|
||||||
Description: "Xwayland support",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
||||||
if u.packageInstalled("accountsservice") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "accountsservice",
|
|
||||||
Status: status,
|
|
||||||
Description: "D-Bus interface for user account query and manipulation",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -151,6 +103,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},
|
||||||
@@ -158,37 +114,60 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"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},
|
|
||||||
"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"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
// Use the cppiber PPA for Hyprland
|
// Use the cppiber PPA for Hyprland
|
||||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
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,
|
||||||
@@ -343,6 +322,19 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := u.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := u.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
u.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.EnableDMSService(ctx, wm); err != nil {
|
||||||
|
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 7: Complete
|
// Phase 7: Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
@@ -365,7 +357,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] {
|
||||||
@@ -545,10 +537,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
buildDeps["libxcb1-dev"] = true
|
buildDeps["libxcb1-dev"] = true
|
||||||
buildDeps["libpipewire-0.3-dev"] = true
|
buildDeps["libpipewire-0.3-dev"] = true
|
||||||
buildDeps["libpam0g-dev"] = true
|
buildDeps["libpam0g-dev"] = true
|
||||||
case "ghostty":
|
|
||||||
buildDeps["curl"] = true
|
|
||||||
buildDeps["libgtk-4-dev"] = true
|
|
||||||
buildDeps["libadwaita-1-dev"] = true
|
|
||||||
case "matugen":
|
case "matugen":
|
||||||
buildDeps["curl"] = true
|
buildDeps["curl"] = true
|
||||||
case "cliphist":
|
case "cliphist":
|
||||||
@@ -562,10 +550,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "ghostty":
|
|
||||||
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Zig: %w", err)
|
|
||||||
}
|
|
||||||
case "cliphist", "dgop":
|
case "cliphist", "dgop":
|
||||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
@@ -629,40 +613,6 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
if u.commandExists("zig") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
|
|
||||||
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
|
|
||||||
|
|
||||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
|
|
||||||
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
|
|
||||||
return fmt.Errorf("failed to download Zig: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
extractCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
|
|
||||||
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
|
|
||||||
return fmt.Errorf("failed to extract Zig: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
linkCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
|
|
||||||
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if u.commandExists("go") {
|
if u.commandExists("go") {
|
||||||
return nil
|
return nil
|
||||||
@@ -710,30 +660,6 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
u.log("Installing Ghostty using Ubuntu installer script...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Running Ghostty Ubuntu installer...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
|
||||||
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
|
||||||
|
|
||||||
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log("Ghostty installed successfully using Ubuntu installer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -743,10 +669,6 @@ func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "ghostty":
|
|
||||||
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -281,6 +286,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, loadInstalledPlugins
|
return m, loadInstalledPlugins
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case pluginUpdatedMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.installedPluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.installedPluginsError = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case pluginInstalledMsg:
|
case pluginInstalledMsg:
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
m.pluginsError = msg.err.Error()
|
m.pluginsError = msg.err.Error()
|
||||||
|
|||||||
@@ -75,14 +75,13 @@ type MenuItem struct {
|
|||||||
|
|
||||||
func NewModel(version string) Model {
|
func NewModel(version string) Model {
|
||||||
detector, _ := NewDetector()
|
detector, _ := NewDetector()
|
||||||
dependencies := detector.GetInstalledComponents()
|
|
||||||
|
|
||||||
// Use the proper detection method for both window managers
|
var dependencies []DependencyInfo
|
||||||
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
var hyprlandInstalled, niriInstalled bool
|
||||||
if err != nil {
|
|
||||||
// Fallback to false if detection fails
|
if detector != nil {
|
||||||
hyprlandInstalled = false
|
dependencies = detector.GetInstalledComponents()
|
||||||
niriInstalled = false
|
hyprlandInstalled, niriInstalled, _ = detector.GetWindowManagerStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
m := Model{
|
m := Model{
|
||||||
@@ -201,6 +200,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, loadInstalledPlugins
|
return m, loadInstalledPlugins
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case pluginUpdatedMsg:
|
||||||
|
if msg.err != nil {
|
||||||
|
m.installedPluginsError = msg.err.Error()
|
||||||
|
} else {
|
||||||
|
m.installedPluginsError = ""
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case pluginInstalledMsg:
|
case pluginInstalledMsg:
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
m.pluginsError = msg.err.Error()
|
m.pluginsError = msg.err.Error()
|
||||||
|
|||||||
@@ -227,6 +227,11 @@ func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd)
|
|||||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||||
return m, uninstallPlugin(plugin)
|
return m, uninstallPlugin(plugin)
|
||||||
}
|
}
|
||||||
|
case "p":
|
||||||
|
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||||
|
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||||
|
return m, updatePlugin(plugin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -246,6 +251,11 @@ type pluginInstalledMsg struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pluginUpdatedMsg struct {
|
||||||
|
pluginName string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
func loadInstalledPlugins() tea.Msg {
|
func loadInstalledPlugins() tea.Msg {
|
||||||
manager, err := plugins.NewManager()
|
manager, err := plugins.NewManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -337,3 +347,31 @@ func uninstallPlugin(plugin pluginInfo) tea.Cmd {
|
|||||||
return pluginUninstalledMsg{pluginName: plugin.Name}
|
return pluginUninstalledMsg{pluginName: plugin.Name}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updatePlugin(plugin pluginInfo) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := plugins.Plugin{
|
||||||
|
ID: plugin.ID,
|
||||||
|
Name: plugin.Name,
|
||||||
|
Category: plugin.Category,
|
||||||
|
Author: plugin.Author,
|
||||||
|
Description: plugin.Description,
|
||||||
|
Repo: plugin.Repo,
|
||||||
|
Path: plugin.Path,
|
||||||
|
Capabilities: plugin.Capabilities,
|
||||||
|
Compositors: plugin.Compositors,
|
||||||
|
Dependencies: plugin.Dependencies,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.Update(p); err != nil {
|
||||||
|
return pluginUpdatedMsg{pluginName: plugin.Name, err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginUpdatedMsg{pluginName: plugin.Name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
|||||||
switch dep.Name {
|
switch dep.Name {
|
||||||
case "dms (DankMaterialShell)", "quickshell":
|
case "dms (DankMaterialShell)", "quickshell":
|
||||||
categories["Shell"] = append(categories["Shell"], dep)
|
categories["Shell"] = append(categories["Shell"], dep)
|
||||||
case "hyprland", "grim", "slurp", "hyprctl", "grimblast":
|
case "hyprland", "hyprctl":
|
||||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||||
case "niri":
|
case "niri":
|
||||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DetectDMSPath checks for DMS installation following XDG Base Directory specification
|
// DetectDMSPath checks for DMS installation following XDG Base Directory specification
|
||||||
@@ -22,10 +23,10 @@ func DetectDMSPath() (string, error) {
|
|||||||
func DetectCompositors() []string {
|
func DetectCompositors() []string {
|
||||||
var compositors []string
|
var compositors []string
|
||||||
|
|
||||||
if commandExists("niri") {
|
if utils.CommandExists("niri") {
|
||||||
compositors = append(compositors, "niri")
|
compositors = append(compositors, "niri")
|
||||||
}
|
}
|
||||||
if commandExists("Hyprland") {
|
if utils.CommandExists("Hyprland") {
|
||||||
compositors = append(compositors, "Hyprland")
|
compositors = append(compositors, "Hyprland")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ func PromptCompositorChoice(compositors []string) (string, error) {
|
|||||||
|
|
||||||
// EnsureGreetdInstalled checks if greetd is installed and installs it if not
|
// EnsureGreetdInstalled checks if greetd is installed and installs it if not
|
||||||
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
||||||
if commandExists("greetd") {
|
if utils.CommandExists("greetd") {
|
||||||
logFunc("✓ greetd is already installed")
|
logFunc("✓ greetd is already installed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
|||||||
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
||||||
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
// Check if dms-greeter is already in PATH
|
// Check if dms-greeter is already in PATH
|
||||||
if commandExists("dms-greeter") {
|
if utils.CommandExists("dms-greeter") {
|
||||||
logFunc("✓ dms-greeter wrapper already installed")
|
logFunc("✓ dms-greeter wrapper already installed")
|
||||||
} else {
|
} else {
|
||||||
// Install the wrapper script
|
// Install the wrapper script
|
||||||
@@ -204,7 +205,7 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
|
|
||||||
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
|
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
|
||||||
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||||
if !commandExists("setfacl") {
|
if !utils.CommandExists("setfacl") {
|
||||||
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
|
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
|
||||||
logFunc(" If theme sync doesn't work, you may need to install acl package:")
|
logFunc(" If theme sync doesn't work, you may need to install acl package:")
|
||||||
logFunc(" - Fedora/RHEL: sudo dnf install acl")
|
logFunc(" - Fedora/RHEL: sudo dnf install acl")
|
||||||
@@ -419,7 +420,7 @@ user = "greeter"
|
|||||||
|
|
||||||
// Determine wrapper command path
|
// Determine wrapper command path
|
||||||
wrapperCmd := "dms-greeter"
|
wrapperCmd := "dms-greeter"
|
||||||
if !commandExists("dms-greeter") {
|
if !utils.CommandExists("dms-greeter") {
|
||||||
wrapperCmd = "/usr/local/bin/dms-greeter"
|
wrapperCmd = "/usr/local/bin/dms-greeter"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,8 +487,3 @@ func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
|||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandExists(cmd string) bool {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscoveryConfig struct {
|
type DiscoveryConfig struct {
|
||||||
@@ -14,13 +16,7 @@ type DiscoveryConfig struct {
|
|||||||
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
||||||
var searchPaths []string
|
var searchPaths []string
|
||||||
|
|
||||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
configHome := utils.XDGConfigHome()
|
||||||
if configHome == "" {
|
|
||||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
|
||||||
configHome = filepath.Join(homeDir, ".config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if configHome != "" {
|
if configHome != "" {
|
||||||
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
|
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
|
||||||
}
|
}
|
||||||
@@ -43,7 +39,7 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
|
|||||||
var files []string
|
var files []string
|
||||||
|
|
||||||
for _, searchPath := range d.SearchPaths {
|
for _, searchPath := range d.SearchPaths {
|
||||||
expandedPath, err := expandPath(searchPath)
|
expandedPath, err := utils.ExpandPath(searchPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -74,20 +70,6 @@ func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandPath(path string) (string, error) {
|
|
||||||
expandedPath := os.ExpandEnv(path)
|
|
||||||
|
|
||||||
if strings.HasPrefix(expandedPath, "~") {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Clean(expandedPath), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONProviderFactory func(filePath string) (Provider, error)
|
type JSONProviderFactory func(filePath string) (Provider, error)
|
||||||
|
|
||||||
var jsonProviderFactory JSONProviderFactory
|
var jsonProviderFactory JSONProviderFactory
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultDiscoveryConfig(t *testing.T) {
|
func TestDefaultDiscoveryConfig(t *testing.T) {
|
||||||
@@ -272,13 +274,13 @@ func TestExpandPathInDiscovery(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result, err := expandPath(tt.input)
|
result, err := utils.ExpandPath(tt.input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expandPath failed: %v", err)
|
t.Fatalf("expandPath failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result != tt.expected {
|
if result != tt.expected {
|
||||||
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
|
t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -42,14 +44,9 @@ func NewHyprlandParser() *HyprlandParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) ReadContent(directory string) error {
|
func (p *HyprlandParser) ReadContent(directory string) error {
|
||||||
expandedDir := os.ExpandEnv(directory)
|
expandedDir, err := utils.ExpandPath(directory)
|
||||||
expandedDir = filepath.Clean(expandedDir)
|
if err != nil {
|
||||||
if strings.HasPrefix(expandedDir, "~") {
|
return err
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
expandedDir = filepath.Join(home, expandedDir[1:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(expandedDir)
|
info, err := os.Stat(expandedDir)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONFileProvider struct {
|
type JSONFileProvider struct {
|
||||||
@@ -20,7 +20,7 @@ func NewJSONFileProvider(filePath string) (*JSONFileProvider, error) {
|
|||||||
return nil, fmt.Errorf("file path cannot be empty")
|
return nil, fmt.Errorf("file path cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
expandedPath, err := expandPath(filePath)
|
expandedPath, err := utils.ExpandPath(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to expand path: %w", err)
|
return nil, fmt.Errorf("failed to expand path: %w", err)
|
||||||
}
|
}
|
||||||
@@ -117,17 +117,3 @@ func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
Binds: categorizedBinds,
|
Binds: categorizedBinds,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func expandPath(path string) (string, error) {
|
|
||||||
expandedPath := os.ExpandEnv(path)
|
|
||||||
|
|
||||||
if strings.HasPrefix(expandedPath, "~") {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Clean(expandedPath), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewJSONFileProvider(t *testing.T) {
|
func TestNewJSONFileProvider(t *testing.T) {
|
||||||
@@ -266,13 +268,13 @@ func TestExpandPath(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result, err := expandPath(tt.input)
|
result, err := utils.ExpandPath(tt.input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expandPath failed: %v", err)
|
t.Fatalf("expandPath failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result != tt.expected {
|
if result != tt.expected {
|
||||||
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
|
t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -34,14 +36,9 @@ func NewMangoWCParser() *MangoWCParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *MangoWCParser) ReadContent(path string) error {
|
func (p *MangoWCParser) ReadContent(path string) error {
|
||||||
expandedPath := os.ExpandEnv(path)
|
expandedPath, err := utils.ExpandPath(path)
|
||||||
expandedPath = filepath.Clean(expandedPath)
|
if err != nil {
|
||||||
if strings.HasPrefix(expandedPath, "~") {
|
return err
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(expandedPath)
|
info, err := os.Stat(expandedPath)
|
||||||
|
|||||||
@@ -3,14 +3,22 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/sblinch/kdl-go"
|
||||||
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NiriProvider struct {
|
type NiriProvider struct {
|
||||||
configDir string
|
configDir string
|
||||||
|
dmsBindsIncluded bool
|
||||||
|
parsed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNiriProvider(configDir string) *NiriProvider {
|
func NewNiriProvider(configDir string) *NiriProvider {
|
||||||
@@ -23,16 +31,7 @@ func NewNiriProvider(configDir string) *NiriProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func defaultNiriConfigDir() string {
|
func defaultNiriConfigDir() string {
|
||||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
return filepath.Join(utils.XDGConfigHome(), "niri")
|
||||||
if 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 {
|
func (n *NiriProvider) Name() string {
|
||||||
@@ -40,22 +39,56 @@ func (n *NiriProvider) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||||
section, err := ParseNiriKeys(n.configDir)
|
result, err := ParseNiriKeys(n.configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse niri config: %w", err)
|
return nil, fmt.Errorf("failed to parse niri config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
n.dmsBindsIncluded = result.DMSBindsIncluded
|
||||||
n.convertSection(section, "", categorizedBinds)
|
n.parsed = true
|
||||||
|
|
||||||
return &keybinds.CheatSheet{
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
Title: "Niri Keybinds",
|
n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
|
||||||
Provider: n.Name(),
|
|
||||||
Binds: categorizedBinds,
|
sheet := &keybinds.CheatSheet{
|
||||||
}, nil
|
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) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
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
|
currentSubcat := subcategory
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
currentSubcat = section.Name
|
currentSubcat = section.Name
|
||||||
@@ -63,12 +96,12 @@ func (n *NiriProvider) convertSection(section *NiriSection, subcategory string,
|
|||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range section.Keybinds {
|
||||||
category := n.categorizeByAction(kb.Action)
|
category := n.categorizeByAction(kb.Action)
|
||||||
bind := n.convertKeybind(&kb, currentSubcat)
|
bind := n.convertKeybind(&kb, currentSubcat, conflicts)
|
||||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, child := range section.Children {
|
for _, child := range section.Children {
|
||||||
n.convertSection(&child, currentSubcat, categorizedBinds)
|
n.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,27 +138,52 @@ func (n *NiriProvider) categorizeByAction(action string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind {
|
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind {
|
||||||
key := n.formatKey(kb)
|
|
||||||
desc := kb.Description
|
|
||||||
rawAction := n.formatRawAction(kb.Action, kb.Args)
|
rawAction := n.formatRawAction(kb.Action, kb.Args)
|
||||||
|
keyStr := n.formatKey(kb)
|
||||||
|
|
||||||
if desc == "" {
|
source := "config"
|
||||||
desc = rawAction
|
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
||||||
|
source = "dms"
|
||||||
}
|
}
|
||||||
|
|
||||||
return keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
Key: key,
|
Key: keyStr,
|
||||||
Description: desc,
|
Description: kb.Description,
|
||||||
Action: rawAction,
|
Action: rawAction,
|
||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
|
Source: source,
|
||||||
|
HideOnOverlay: kb.HideOnOverlay,
|
||||||
|
CooldownMs: kb.CooldownMs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (n *NiriProvider) formatRawAction(action string, args []string) string {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return action
|
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, " ")
|
return action + " " + strings.Join(args, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,3 +193,395 @@ func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
|
|||||||
parts = append(parts, kb.Key)
|
parts = append(parts, kb.Key)
|
||||||
return strings.Join(parts, "+")
|
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 {
|
||||||
|
if ms, err := strconv.Atoi(val.String()); err == nil {
|
||||||
|
opts["cooldown-ms"] = ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case int:
|
||||||
|
node.AddProperty("cooldown-ms", val, "")
|
||||||
|
case string:
|
||||||
|
if ms, err := strconv.Atoi(val); err == nil {
|
||||||
|
node.AddProperty("cooldown-ms", ms, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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:] {
|
||||||
|
if strings.Contains(arg, "=") {
|
||||||
|
kv := strings.SplitN(arg, "=", 2)
|
||||||
|
switch kv[1] {
|
||||||
|
case "true":
|
||||||
|
node.AddProperty(kv[0], true, "")
|
||||||
|
case "false":
|
||||||
|
node.AddProperty(kv[0], false, "")
|
||||||
|
default:
|
||||||
|
node.AddProperty(kv[0], kv[1], "")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
node.AddArgument(arg, "")
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) parseActionParts(action string) []string {
|
||||||
|
var parts []string
|
||||||
|
var current strings.Builder
|
||||||
|
var inQuote, escaped, wasQuoted bool
|
||||||
|
|
||||||
|
for _, r := range action {
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
|
current.WriteRune(r)
|
||||||
|
escaped = false
|
||||||
|
case r == '\\':
|
||||||
|
escaped = true
|
||||||
|
case r == '"':
|
||||||
|
wasQuoted = true
|
||||||
|
inQuote = !inQuote
|
||||||
|
case r == ' ' && !inQuote:
|
||||||
|
if current.Len() > 0 || wasQuoted {
|
||||||
|
parts = append(parts, current.String())
|
||||||
|
current.Reset()
|
||||||
|
wasQuoted = false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
current.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current.Len() > 0 || wasQuoted {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if child.Properties.Exist() {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(strings.TrimLeft(child.Properties.String(), " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sblinch/kdl-go"
|
"github.com/sblinch/kdl-go"
|
||||||
@@ -11,11 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type NiriKeyBinding struct {
|
type NiriKeyBinding struct {
|
||||||
Mods []string
|
Mods []string
|
||||||
Key string
|
Key string
|
||||||
Action string
|
Action string
|
||||||
Args []string
|
Args []string
|
||||||
Description string
|
Description string
|
||||||
|
HideOnOverlay bool
|
||||||
|
CooldownMs int
|
||||||
|
Source string
|
||||||
}
|
}
|
||||||
|
|
||||||
type NiriSection struct {
|
type NiriSection struct {
|
||||||
@@ -25,32 +29,78 @@ type NiriSection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NiriParser struct {
|
type NiriParser struct {
|
||||||
configDir string
|
configDir string
|
||||||
processedFiles map[string]bool
|
processedFiles map[string]bool
|
||||||
bindMap map[string]*NiriKeyBinding
|
bindMap map[string]*NiriKeyBinding
|
||||||
bindOrder []string
|
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 {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
return &NiriParser{
|
return &NiriParser{
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
processedFiles: make(map[string]bool),
|
processedFiles: make(map[string]bool),
|
||||||
bindMap: make(map[string]*NiriKeyBinding),
|
bindMap: make(map[string]*NiriKeyBinding),
|
||||||
bindOrder: []string{},
|
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) {
|
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")
|
configPath := filepath.Join(p.configDir, "config.kdl")
|
||||||
section, err := p.parseFile(configPath, "")
|
section, err := p.parseFile(configPath, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.dmsBindsExists && !p.dmsProcessed {
|
||||||
|
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
||||||
|
}
|
||||||
|
|
||||||
section.Keybinds = p.finalizeBinds()
|
section.Keybinds = p.finalizeBinds()
|
||||||
return section, nil
|
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 {
|
func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
|
||||||
binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
|
binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
|
||||||
for _, key := range p.bindOrder {
|
for _, key := range p.bindOrder {
|
||||||
@@ -63,6 +113,20 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
|
|||||||
|
|
||||||
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
|
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
|
||||||
key := p.formatBindKey(kb)
|
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 {
|
if _, exists := p.bindMap[key]; !exists {
|
||||||
p.bindOrder = append(p.bindOrder, key)
|
p.bindOrder = append(p.bindOrder, key)
|
||||||
}
|
}
|
||||||
@@ -101,8 +165,11 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
|
|||||||
Name: sectionName,
|
Name: sectionName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
baseDir := filepath.Dir(absPath)
|
baseDir := filepath.Dir(absPath)
|
||||||
p.processNodes(doc.Nodes, section, baseDir)
|
p.processNodes(doc.Nodes, section, baseDir)
|
||||||
|
p.currentSource = prevSource
|
||||||
|
|
||||||
return section, nil
|
return section, nil
|
||||||
}
|
}
|
||||||
@@ -127,14 +194,23 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
includePath := node.Arguments[0].String()
|
includePath := strings.Trim(node.Arguments[0].String(), "\"")
|
||||||
includePath = strings.Trim(includePath, "\"")
|
isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl")
|
||||||
|
|
||||||
var fullPath string
|
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) {
|
if filepath.IsAbs(includePath) {
|
||||||
fullPath = includePath
|
fullPath = includePath
|
||||||
} else {
|
}
|
||||||
fullPath = filepath.Join(baseDir, includePath)
|
|
||||||
|
if isDMSInclude {
|
||||||
|
p.dmsProcessed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
includedSection, err := p.parseFile(fullPath, "")
|
includedSection, err := p.parseFile(fullPath, "")
|
||||||
@@ -145,6 +221,10 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
|
|||||||
section.Children = append(section.Children, includedSection.Children...)
|
section.Children = append(section.Children, includedSection.Children...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *NiriParser) HasDMSBindsIncluded() bool {
|
||||||
|
return p.dmsBindsIncluded
|
||||||
|
}
|
||||||
|
|
||||||
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
|
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
|
||||||
if node.Children == nil {
|
if node.Children == nil {
|
||||||
return
|
return
|
||||||
@@ -172,7 +252,7 @@ func (p *NiriParser) extractBinds(node *document.Node, section *NiriSection, sub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *NiriKeyBinding {
|
func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBinding {
|
||||||
keyCombo := node.Name.String()
|
keyCombo := node.Name.String()
|
||||||
if keyCombo == "" {
|
if keyCombo == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -182,48 +262,117 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *
|
|||||||
|
|
||||||
var action string
|
var action string
|
||||||
var args []string
|
var args []string
|
||||||
|
|
||||||
if len(node.Children) > 0 {
|
if len(node.Children) > 0 {
|
||||||
actionNode := node.Children[0]
|
actionNode := node.Children[0]
|
||||||
action = actionNode.Name.String()
|
action = actionNode.Name.String()
|
||||||
for _, arg := range actionNode.Arguments {
|
for _, arg := range actionNode.Arguments {
|
||||||
args = append(args, strings.Trim(arg.String(), "\""))
|
args = append(args, arg.ValueString())
|
||||||
|
}
|
||||||
|
if actionNode.Properties != nil {
|
||||||
|
if val, ok := actionNode.Properties.Get("focus"); ok {
|
||||||
|
args = append(args, "focus="+val.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
description := ""
|
var description string
|
||||||
|
var hideOnOverlay bool
|
||||||
|
var cooldownMs int
|
||||||
if node.Properties != nil {
|
if node.Properties != nil {
|
||||||
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
||||||
description = strings.Trim(val.String(), "\"")
|
switch val.ValueString() {
|
||||||
|
case "null", "":
|
||||||
|
hideOnOverlay = true
|
||||||
|
default:
|
||||||
|
description = val.ValueString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("cooldown-ms"); ok {
|
||||||
|
cooldownMs, _ = strconv.Atoi(val.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &NiriKeyBinding{
|
return &NiriKeyBinding{
|
||||||
Mods: mods,
|
Mods: mods,
|
||||||
Key: key,
|
Key: key,
|
||||||
Action: action,
|
Action: action,
|
||||||
Args: args,
|
Args: args,
|
||||||
Description: description,
|
Description: description,
|
||||||
|
HideOnOverlay: hideOnOverlay,
|
||||||
|
CooldownMs: cooldownMs,
|
||||||
|
Source: p.currentSource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
|
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
|
||||||
parts := strings.Split(combo, "+")
|
parts := strings.Split(combo, "+")
|
||||||
if len(parts) == 0 {
|
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
return nil, combo
|
return nil, combo
|
||||||
}
|
case 1:
|
||||||
|
|
||||||
if len(parts) == 1 {
|
|
||||||
return nil, parts[0]
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
mods := parts[:len(parts)-1]
|
switch {
|
||||||
key := parts[len(parts)-1]
|
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 mods, key
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseNiriKeys(configDir string) (*NiriSection, error) {
|
func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
|
||||||
parser := NewNiriParser(configDir)
|
parser := NewNiriParser(configDir)
|
||||||
return parser.Parse()
|
section, err := parser.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &NiriParseResult{
|
||||||
|
Section: section,
|
||||||
|
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
|
||||||
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,20 +57,20 @@ func TestNiriParseBasicBinds(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 3 {
|
if len(result.Section.Keybinds) != 3 {
|
||||||
t.Errorf("Expected 3 keybinds, got %d", len(section.Keybinds))
|
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
foundClose := false
|
foundClose := false
|
||||||
foundFullscreen := false
|
foundFullscreen := false
|
||||||
foundTerminal := false
|
foundTerminal := false
|
||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range result.Section.Keybinds {
|
||||||
switch kb.Action {
|
switch kb.Action {
|
||||||
case "close-window":
|
case "close-window":
|
||||||
foundClose = true
|
foundClose = true
|
||||||
@@ -116,19 +116,19 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 2 {
|
if len(result.Section.Keybinds) != 2 {
|
||||||
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
foundNext := false
|
foundNext := false
|
||||||
foundPrev := false
|
foundPrev := false
|
||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range result.Section.Keybinds {
|
||||||
switch kb.Action {
|
switch kb.Action {
|
||||||
case "next-window":
|
case "next-window":
|
||||||
foundNext = true
|
foundNext = true
|
||||||
@@ -172,13 +172,13 @@ include "dms/binds.kdl"
|
|||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 2 {
|
if len(result.Section.Keybinds) != 2 {
|
||||||
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,17 +209,17 @@ include "dms/binds.kdl"
|
|||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 1 {
|
if len(result.Section.Keybinds) != 1 {
|
||||||
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(section.Keybinds))
|
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) > 0 {
|
if len(result.Section.Keybinds) > 0 {
|
||||||
kb := section.Keybinds[0]
|
kb := result.Section.Keybinds[0]
|
||||||
if kb.Description != "Override Terminal" {
|
if kb.Description != "Override Terminal" {
|
||||||
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
|
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
|
||||||
}
|
}
|
||||||
@@ -253,13 +253,13 @@ include "config.kdl"
|
|||||||
t.Fatalf("Failed to write other config: %v", err)
|
t.Fatalf("Failed to write other config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
|
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 2 {
|
if len(result.Section.Keybinds) != 2 {
|
||||||
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(section.Keybinds))
|
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,13 +276,13 @@ include "nonexistent/file.kdl"
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
|
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 1 {
|
if len(result.Section.Keybinds) != 1 {
|
||||||
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(section.Keybinds))
|
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,13 +305,13 @@ input {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 0 {
|
if len(result.Section.Keybinds) != 0 {
|
||||||
t.Errorf("Expected 0 keybinds, got %d", len(section.Keybinds))
|
t.Errorf("Expected 0 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,18 +352,18 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 3 {
|
if len(result.Section.Keybinds) != 3 {
|
||||||
t.Fatalf("Expected 3 unique keybinds, got %d", len(section.Keybinds))
|
t.Fatalf("Expected 3 unique keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
var modT *NiriKeyBinding
|
var modT *NiriKeyBinding
|
||||||
for i := range section.Keybinds {
|
for i := range result.Section.Keybinds {
|
||||||
kb := §ion.Keybinds[i]
|
kb := &result.Section.Keybinds[i]
|
||||||
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
|
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
|
||||||
modT = kb
|
modT = kb
|
||||||
break
|
break
|
||||||
@@ -416,18 +416,18 @@ binds {
|
|||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 4 {
|
if len(result.Section.Keybinds) != 4 {
|
||||||
t.Errorf("Expected 4 unique keybinds, got %d", len(section.Keybinds))
|
t.Errorf("Expected 4 unique keybinds, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
bindMap := make(map[string]*NiriKeyBinding)
|
bindMap := make(map[string]*NiriKeyBinding)
|
||||||
for i := range section.Keybinds {
|
for i := range result.Section.Keybinds {
|
||||||
kb := §ion.Keybinds[i]
|
kb := &result.Section.Keybinds[i]
|
||||||
key := ""
|
key := ""
|
||||||
for _, m := range kb.Mods {
|
for _, m := range kb.Mods {
|
||||||
key += m + "+"
|
key += m + "+"
|
||||||
@@ -475,16 +475,16 @@ func TestNiriParseMultipleArgs(t *testing.T) {
|
|||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
section, err := ParseNiriKeys(tmpDir)
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(section.Keybinds) != 1 {
|
if len(result.Section.Keybinds) != 1 {
|
||||||
t.Fatalf("Expected 1 keybind, got %d", len(section.Keybinds))
|
t.Fatalf("Expected 1 keybind, got %d", len(result.Section.Keybinds))
|
||||||
}
|
}
|
||||||
|
|
||||||
kb := section.Keybinds[0]
|
kb := result.Section.Keybinds[0]
|
||||||
if len(kb.Args) != 5 {
|
if len(kb.Args) != 5 {
|
||||||
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
|
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
|
||||||
}
|
}
|
||||||
@@ -496,3 +496,135 @@ func TestNiriParseMultipleArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) != 2 {
|
||||||
|
t.Errorf("move-column-to-workspace should have 2 args (index + focus), got %d", len(kb.Args))
|
||||||
|
}
|
||||||
|
hasIndex := false
|
||||||
|
hasFocus := false
|
||||||
|
for _, arg := range kb.Args {
|
||||||
|
if arg == "1" || arg == "2" {
|
||||||
|
hasIndex = true
|
||||||
|
}
|
||||||
|
if arg == "focus=false" {
|
||||||
|
hasFocus = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasIndex {
|
||||||
|
t.Errorf("move-column-to-workspace missing index arg")
|
||||||
|
}
|
||||||
|
if !hasFocus {
|
||||||
|
t.Errorf("move-column-to-workspace missing focus=false arg")
|
||||||
|
}
|
||||||
|
case "next-window":
|
||||||
|
if kb.Key != "Tab" {
|
||||||
|
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -186,6 +186,144 @@ func TestNiriDefaultConfigDir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestNiriProviderWithRealWorldConfig(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
@@ -259,3 +397,211 @@ recent-windows {
|
|||||||
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package providers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
@@ -9,18 +10,42 @@ import (
|
|||||||
|
|
||||||
type SwayProvider struct {
|
type SwayProvider struct {
|
||||||
configPath string
|
configPath string
|
||||||
|
isScroll bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSwayProvider(configPath string) *SwayProvider {
|
func NewSwayProvider(configPath string) *SwayProvider {
|
||||||
|
isScroll := false
|
||||||
|
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
|
||||||
|
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
configPath = "$HOME/.config/sway"
|
if scrollEnvSet {
|
||||||
|
configPath = "$HOME/.config/scroll"
|
||||||
|
isScroll = true
|
||||||
|
} else {
|
||||||
|
configPath = "$HOME/.config/sway"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Determine isScroll based on the provided config path
|
||||||
|
isScroll = strings.Contains(configPath, "scroll")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SwayProvider{
|
return &SwayProvider{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
|
isScroll: isScroll,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SwayProvider) Name() string {
|
func (s *SwayProvider) Name() string {
|
||||||
|
if s != nil && s.isScroll {
|
||||||
|
return "scroll"
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
_, ok := os.LookupEnv("SCROLLSOCK")
|
||||||
|
if ok {
|
||||||
|
return "scroll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "sway"
|
return "sway"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +58,13 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
s.convertSection(section, "", categorizedBinds)
|
s.convertSection(section, "", categorizedBinds)
|
||||||
|
|
||||||
|
cheatSheetTitle := "Sway Keybinds"
|
||||||
|
if s != nil && s.isScroll {
|
||||||
|
cheatSheetTitle = "Scroll Keybinds"
|
||||||
|
}
|
||||||
|
|
||||||
return &keybinds.CheatSheet{
|
return &keybinds.CheatSheet{
|
||||||
Title: "Sway Keybinds",
|
Title: cheatSheetTitle,
|
||||||
Provider: s.Name(),
|
Provider: s.Name(),
|
||||||
Binds: categorizedBinds,
|
Binds: categorizedBinds,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -42,14 +44,9 @@ func NewSwayParser() *SwayParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *SwayParser) ReadContent(path string) error {
|
func (p *SwayParser) ReadContent(path string) error {
|
||||||
expandedPath := os.ExpandEnv(path)
|
expandedPath, err := utils.ExpandPath(path)
|
||||||
expandedPath = filepath.Clean(expandedPath)
|
if err != nil {
|
||||||
if strings.HasPrefix(expandedPath, "~") {
|
return err
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(expandedPath)
|
info, err := os.Stat(expandedPath)
|
||||||
|
|||||||
@@ -1,19 +1,43 @@
|
|||||||
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"`
|
||||||
Action string `json:"action,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
Subcategory string `json:"subcat,omitempty"`
|
Subcategory string `json:"subcat,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
||||||
|
CooldownMs int `json:"cooldownMs,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
|
||||||
|
}
|
||||||
|
|||||||
583
core/internal/matugen/matugen.go
Normal file
583
core/internal/matugen/matugen.go
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
package matugen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
matugenVersionOnce sync.Once
|
||||||
|
matugenSupportsCOE bool
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
StateDir string
|
||||||
|
ShellDir string
|
||||||
|
ConfigDir string
|
||||||
|
Kind string
|
||||||
|
Value string
|
||||||
|
Mode string
|
||||||
|
IconTheme string
|
||||||
|
MatugenType string
|
||||||
|
RunUserTemplates bool
|
||||||
|
StockColors string
|
||||||
|
SyncModeWithPortal bool
|
||||||
|
TerminalsAlwaysDark bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColorsOutput struct {
|
||||||
|
Colors struct {
|
||||||
|
Dark map[string]string `json:"dark"`
|
||||||
|
Light map[string]string `json:"light"`
|
||||||
|
} `json:"colors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) ColorsOutput() string {
|
||||||
|
return filepath.Join(o.StateDir, "dms-colors.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(opts Options) error {
|
||||||
|
if opts.StateDir == "" {
|
||||||
|
return fmt.Errorf("state-dir is required")
|
||||||
|
}
|
||||||
|
if opts.ShellDir == "" {
|
||||||
|
return fmt.Errorf("shell-dir is required")
|
||||||
|
}
|
||||||
|
if opts.ConfigDir == "" {
|
||||||
|
return fmt.Errorf("config-dir is required")
|
||||||
|
}
|
||||||
|
if opts.Kind == "" {
|
||||||
|
return fmt.Errorf("kind is required")
|
||||||
|
}
|
||||||
|
if opts.Value == "" {
|
||||||
|
return fmt.Errorf("value is required")
|
||||||
|
}
|
||||||
|
if opts.Mode == "" {
|
||||||
|
opts.Mode = "dark"
|
||||||
|
}
|
||||||
|
if opts.MatugenType == "" {
|
||||||
|
opts.MatugenType = "scheme-tonal-spot"
|
||||||
|
}
|
||||||
|
if opts.IconTheme == "" {
|
||||||
|
opts.IconTheme = "System Default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create state dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
||||||
|
|
||||||
|
if err := buildOnce(&opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.SyncModeWithPortal {
|
||||||
|
syncColorScheme(opts.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Done")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOnce(opts *Options) error {
|
||||||
|
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp config: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to build config: %w", err)
|
||||||
|
}
|
||||||
|
cfgFile.Close()
|
||||||
|
|
||||||
|
var primaryDark, primaryLight, surface string
|
||||||
|
var dank16JSON string
|
||||||
|
var importArgs []string
|
||||||
|
|
||||||
|
if opts.StockColors != "" {
|
||||||
|
log.Info("Using stock/custom theme colors with matugen base")
|
||||||
|
primaryDark = extractNestedColor(opts.StockColors, "primary", "dark")
|
||||||
|
primaryLight = extractNestedColor(opts.StockColors, "primary", "light")
|
||||||
|
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||||
|
|
||||||
|
if primaryDark == "" {
|
||||||
|
return fmt.Errorf("failed to extract primary dark from stock colors")
|
||||||
|
}
|
||||||
|
if primaryLight == "" {
|
||||||
|
primaryLight = primaryDark
|
||||||
|
}
|
||||||
|
|
||||||
|
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||||
|
importData := fmt.Sprintf(`{"colors": %s, "dank16": %s}`, opts.StockColors, dank16JSON)
|
||||||
|
importArgs = []string{"--import-json-string", importData}
|
||||||
|
|
||||||
|
log.Info("Running matugen color hex with stock color overrides")
|
||||||
|
args := []string{"color", "hex", primaryDark, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
|
args = append(args, importArgs...)
|
||||||
|
if err := runMatugen(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||||
|
|
||||||
|
matJSON, err := runMatugenDryRun(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("matugen dry-run failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||||
|
primaryLight = extractMatugenColor(matJSON, "primary", "light")
|
||||||
|
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||||
|
|
||||||
|
if primaryDark == "" {
|
||||||
|
return fmt.Errorf("failed to extract primary color")
|
||||||
|
}
|
||||||
|
if primaryLight == "" {
|
||||||
|
primaryLight = primaryDark
|
||||||
|
}
|
||||||
|
|
||||||
|
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||||
|
importData := fmt.Sprintf(`{"dank16": %s}`, dank16JSON)
|
||||||
|
importArgs = []string{"--import-json-string", importData}
|
||||||
|
|
||||||
|
log.Infof("Running matugen %s with dank16 injection", opts.Kind)
|
||||||
|
var args []string
|
||||||
|
switch opts.Kind {
|
||||||
|
case "hex":
|
||||||
|
args = []string{"color", "hex", opts.Value}
|
||||||
|
default:
|
||||||
|
args = []string{opts.Kind, opts.Value}
|
||||||
|
}
|
||||||
|
args = append(args, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
|
args = append(args, importArgs...)
|
||||||
|
if err := runMatugen(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshGTK(opts.ConfigDir, opts.Mode)
|
||||||
|
signalTerminals()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||||
|
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||||
|
|
||||||
|
wroteConfig := false
|
||||||
|
if opts.RunUserTemplates {
|
||||||
|
if data, err := os.ReadFile(userConfigPath); err == nil {
|
||||||
|
configSection := extractTOMLSection(string(data), "[config]", "[templates]")
|
||||||
|
if configSection != "" {
|
||||||
|
cfgFile.WriteString(configSection)
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
wroteConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wroteConfig {
|
||||||
|
cfgFile.WriteString("[config]\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfigPath := filepath.Join(opts.ShellDir, "matugen", "configs", "base.toml")
|
||||||
|
if data, err := os.ReadFile(baseConfigPath); err == nil {
|
||||||
|
content := string(data)
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) == "[config]" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfgFile.WriteString(substituteShellDir(line, opts.ShellDir) + "\n")
|
||||||
|
}
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(cfgFile, `[templates.dank]
|
||||||
|
input_path = '%s/matugen/templates/dank.json'
|
||||||
|
output_path = '%s'
|
||||||
|
|
||||||
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
switch opts.Mode {
|
||||||
|
case "light":
|
||||||
|
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
||||||
|
default:
|
||||||
|
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
||||||
|
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
||||||
|
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
||||||
|
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
||||||
|
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
||||||
|
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
||||||
|
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
||||||
|
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
|
||||||
|
if opts.RunUserTemplates {
|
||||||
|
if data, err := os.ReadFile(userConfigPath); err == nil {
|
||||||
|
templatesSection := extractTOMLSection(string(data), "[templates]", "")
|
||||||
|
if templatesSection != "" {
|
||||||
|
cfgFile.WriteString(templatesSection)
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userPluginConfigDir := filepath.Join(opts.ConfigDir, "matugen", "dms", "configs")
|
||||||
|
if entries, err := os.ReadDir(userPluginConfigDir); err == nil {
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !strings.HasSuffix(entry.Name(), ".toml") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data, err := os.ReadFile(filepath.Join(userPluginConfigDir, entry.Name())); err == nil {
|
||||||
|
cfgFile.WriteString(string(data))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
|
||||||
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfgFile.WriteString(substituteShellDir(string(data), opts.ShellDir))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fileName string) {
|
||||||
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if checkCmd != "skip" && !utils.CommandExists(checkCmd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
if !opts.TerminalsAlwaysDark {
|
||||||
|
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if !strings.Contains(line, "input_path") || !strings.Contains(line, "SHELL_DIR/matugen/templates/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
start := strings.Index(line, "'SHELL_DIR/matugen/templates/")
|
||||||
|
if start == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end := strings.Index(line[start+1:], "'")
|
||||||
|
if end == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
templateName := line[start+len("'SHELL_DIR/matugen/templates/") : start+1+end]
|
||||||
|
origPath := filepath.Join(opts.ShellDir, "matugen", "templates", templateName)
|
||||||
|
|
||||||
|
origData, err := os.ReadFile(origPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
||||||
|
tmpPath := filepath.Join(tmpDir, templateName)
|
||||||
|
if err := os.WriteFile(tmpPath, []byte(modified), 0644); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
content = strings.ReplaceAll(content,
|
||||||
|
fmt.Sprintf("'SHELL_DIR/matugen/templates/%s'", templateName),
|
||||||
|
fmt.Sprintf("'%s'", tmpPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
|
||||||
|
if _, err := os.Stat(extDir); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateDir := filepath.Join(shellDir, "matugen", "templates")
|
||||||
|
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
|
||||||
|
input_path = '%s/vscode-color-theme-default.json'
|
||||||
|
output_path = '%s/themes/dankshell-default.json'
|
||||||
|
|
||||||
|
[templates.dms%sdark]
|
||||||
|
input_path = '%s/vscode-color-theme-dark.json'
|
||||||
|
output_path = '%s/themes/dankshell-dark.json'
|
||||||
|
|
||||||
|
[templates.dms%slight]
|
||||||
|
input_path = '%s/vscode-color-theme-light.json'
|
||||||
|
output_path = '%s/themes/dankshell-light.json'
|
||||||
|
|
||||||
|
`, name, templateDir, extDir,
|
||||||
|
name, templateDir, extDir,
|
||||||
|
name, templateDir, extDir)
|
||||||
|
log.Infof("Added %s theme config (extension found at %s)", name, extDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func substituteShellDir(content, shellDir string) string {
|
||||||
|
return strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTOMLSection(content, startMarker, endMarker string) string {
|
||||||
|
startIdx := strings.Index(content, startMarker)
|
||||||
|
if startIdx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if endMarker == "" {
|
||||||
|
return content[startIdx:]
|
||||||
|
}
|
||||||
|
|
||||||
|
endIdx := strings.Index(content[startIdx:], endMarker)
|
||||||
|
if endIdx == -1 {
|
||||||
|
return content[startIdx:]
|
||||||
|
}
|
||||||
|
return content[startIdx : startIdx+endIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMatugenVersion() {
|
||||||
|
matugenVersionOnce.Do(func() {
|
||||||
|
cmd := exec.Command("matugen", "--version")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStr := strings.TrimSpace(string(output))
|
||||||
|
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
||||||
|
|
||||||
|
parts := strings.Split(versionStr, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
minor, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
||||||
|
if matugenSupportsCOE {
|
||||||
|
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugen(args []string) error {
|
||||||
|
checkMatugenVersion()
|
||||||
|
|
||||||
|
if matugenSupportsCOE {
|
||||||
|
args = append([]string{"--continue-on-error"}, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("matugen", args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugenDryRun(opts *Options) (string, error) {
|
||||||
|
var args []string
|
||||||
|
switch opts.Kind {
|
||||||
|
case "hex":
|
||||||
|
args = []string{"color", "hex", opts.Value}
|
||||||
|
default:
|
||||||
|
args = []string{opts.Kind, opts.Value}
|
||||||
|
}
|
||||||
|
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||||
|
|
||||||
|
cmd := exec.Command("matugen", args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(string(output), "\n", ""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMatugenColor(jsonStr, colorName, variant string) string {
|
||||||
|
var data map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
colors, ok := data["colors"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
colorData, ok := colors[colorName].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variantData, ok := colorData[variant].(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return variantData
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractNestedColor(jsonStr, colorName, variant string) string {
|
||||||
|
var data map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
colorData, ok := data[colorName].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variantData, ok := colorData[variant].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
color, ok := variantData["color"].(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateDank16Variants(primaryDark, primaryLight, surface, mode string) string {
|
||||||
|
variantOpts := dank16.VariantOptions{
|
||||||
|
PrimaryDark: primaryDark,
|
||||||
|
PrimaryLight: primaryLight,
|
||||||
|
Background: surface,
|
||||||
|
UseDPS: true,
|
||||||
|
IsLightMode: mode == "light",
|
||||||
|
}
|
||||||
|
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||||
|
return dank16.GenerateVariantJSON(variantColors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshGTK(configDir, mode string) {
|
||||||
|
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
||||||
|
|
||||||
|
info, err := os.Lstat(gtkCSS)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRun := false
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
target, err := os.Readlink(gtkCSS)
|
||||||
|
if err == nil && strings.Contains(target, "dank-colors.css") {
|
||||||
|
shouldRun = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err := os.ReadFile(gtkCSS)
|
||||||
|
if err == nil && strings.Contains(string(data), "dank-colors.css") {
|
||||||
|
shouldRun = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldRun {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
||||||
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalTerminals() {
|
||||||
|
signalByName("kitty", syscall.SIGUSR1)
|
||||||
|
signalByName("ghostty", syscall.SIGUSR2)
|
||||||
|
signalByName(".kitty-wrapped", syscall.SIGUSR1)
|
||||||
|
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalByName(name string, sig syscall.Signal) {
|
||||||
|
entries, err := os.ReadDir("/proc")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
pid, err := strconv.Atoi(entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(comm)) == name {
|
||||||
|
syscall.Kill(pid, sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncColorScheme(mode string) {
|
||||||
|
scheme := "prefer-dark"
|
||||||
|
if mode == "light" {
|
||||||
|
scheme = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
|
||||||
|
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
139
core/internal/matugen/queue.go
Normal file
139
core/internal/matugen/queue.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package matugen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueuedJob struct {
|
||||||
|
Options Options
|
||||||
|
Done chan Result
|
||||||
|
Ctx context.Context
|
||||||
|
Cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queue struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
current *QueuedJob
|
||||||
|
pending *QueuedJob
|
||||||
|
jobDone chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalQueue *Queue
|
||||||
|
var queueOnce sync.Once
|
||||||
|
|
||||||
|
func GetQueue() *Queue {
|
||||||
|
queueOnce.Do(func() {
|
||||||
|
globalQueue = &Queue{
|
||||||
|
jobDone: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return globalQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) Submit(opts Options) <-chan Result {
|
||||||
|
result := make(chan Result, 1)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
job := &QueuedJob{
|
||||||
|
Options: opts,
|
||||||
|
Done: result,
|
||||||
|
Ctx: ctx,
|
||||||
|
Cancel: cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
q.mu.Lock()
|
||||||
|
|
||||||
|
if q.pending != nil {
|
||||||
|
log.Info("Cancelling pending theme request")
|
||||||
|
q.pending.Cancel()
|
||||||
|
q.pending.Done <- Result{Success: false, Error: context.Canceled}
|
||||||
|
close(q.pending.Done)
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.current != nil {
|
||||||
|
q.pending = job
|
||||||
|
q.mu.Unlock()
|
||||||
|
log.Info("Theme request queued (worker running)")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
q.current = job
|
||||||
|
q.mu.Unlock()
|
||||||
|
|
||||||
|
go q.runWorker()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) runWorker() {
|
||||||
|
for {
|
||||||
|
q.mu.Lock()
|
||||||
|
job := q.current
|
||||||
|
if job == nil {
|
||||||
|
q.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q.mu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-job.Ctx.Done():
|
||||||
|
q.finishJob(Result{Success: false, Error: context.Canceled})
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Processing theme: %s %s (%s)", job.Options.Kind, job.Options.Value, job.Options.Mode)
|
||||||
|
err := Run(job.Options)
|
||||||
|
|
||||||
|
var result Result
|
||||||
|
if err != nil {
|
||||||
|
result = Result{Success: false, Error: err}
|
||||||
|
} else {
|
||||||
|
result = Result{Success: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
q.finishJob(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) finishJob(result Result) {
|
||||||
|
q.mu.Lock()
|
||||||
|
defer q.mu.Unlock()
|
||||||
|
|
||||||
|
if q.current != nil {
|
||||||
|
select {
|
||||||
|
case q.current.Done <- result:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
close(q.current.Done)
|
||||||
|
}
|
||||||
|
|
||||||
|
q.current = q.pending
|
||||||
|
q.pending = nil
|
||||||
|
|
||||||
|
if q.current == nil {
|
||||||
|
select {
|
||||||
|
case q.jobDone <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) IsRunning() bool {
|
||||||
|
q.mu.Lock()
|
||||||
|
defer q.mu.Unlock()
|
||||||
|
return q.current != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) HasPending() bool {
|
||||||
|
q.mu.Lock()
|
||||||
|
defer q.mu.Unlock()
|
||||||
|
return q.pending != nil
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,33 +33,70 @@ func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getPluginsDir() string {
|
func getPluginsDir() string {
|
||||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
return filepath.Join(utils.XDGConfigHome(), "DankMaterialShell", "plugins")
|
||||||
if configHome == "" {
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return filepath.Join(os.TempDir(), "DankMaterialShell", "plugins")
|
|
||||||
}
|
|
||||||
configHome = filepath.Join(homeDir, ".config")
|
|
||||||
}
|
|
||||||
return filepath.Join(configHome, "DankMaterialShell", "plugins")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
|
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
|
||||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
path, err := m.findInstalledPath(plugin.ID)
|
||||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if exists {
|
return path != "", nil
|
||||||
return true, nil
|
}
|
||||||
|
|
||||||
|
func (m *Manager) findInstalledPath(pluginID string) (string, error) {
|
||||||
|
// Check user plugins directory
|
||||||
|
path, err := m.findInDir(m.pluginsDir, pluginID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
// Check system plugins directory
|
||||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
systemDir := "/etc/xdg/quickshell/dms-plugins"
|
||||||
if err != nil {
|
return m.findInDir(systemDir, pluginID)
|
||||||
return false, err
|
}
|
||||||
|
|
||||||
|
func (m *Manager) findInDir(dir, pluginID string) (string, error) {
|
||||||
|
// First, check if folder with exact ID name exists
|
||||||
|
exactPath := filepath.Join(dir, pluginID)
|
||||||
|
if exists, _ := afero.DirExists(m.fs, exactPath); exists {
|
||||||
|
return exactPath, nil
|
||||||
}
|
}
|
||||||
return systemExists, nil
|
|
||||||
|
// Scan all folders and check plugin.json for matching ID
|
||||||
|
exists, err := afero.DirExists(m.fs, dir)
|
||||||
|
if err != nil || !exists {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := afero.ReadDir(m.fs, dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if name == ".repos" || strings.HasSuffix(name, ".meta") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(dir, name)
|
||||||
|
isPlugin := entry.IsDir() || entry.Mode()&os.ModeSymlink != 0
|
||||||
|
if !isPlugin {
|
||||||
|
if info, err := m.fs.Stat(fullPath); err == nil && info.IsDir() {
|
||||||
|
isPlugin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPlugin && m.getPluginID(fullPath) == pluginID {
|
||||||
|
return fullPath, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Install(plugin Plugin) error {
|
func (m *Manager) Install(plugin Plugin) error {
|
||||||
@@ -151,25 +189,19 @@ func (m *Manager) createSymlink(source, dest string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Update(plugin Plugin) error {
|
func (m *Manager) Update(plugin Plugin) error {
|
||||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
pluginPath, err := m.findInstalledPath(plugin.ID)
|
||||||
|
|
||||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
return fmt.Errorf("failed to find plugin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if pluginPath == "" {
|
||||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
|
||||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
|
||||||
}
|
|
||||||
if systemExists {
|
|
||||||
return fmt.Errorf("cannot update system plugin: %s", plugin.Name)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
|
||||||
|
return fmt.Errorf("cannot update system plugin: %s", plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
metaPath := pluginPath + ".meta"
|
metaPath := pluginPath + ".meta"
|
||||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -209,25 +241,19 @@ func (m *Manager) Update(plugin Plugin) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Uninstall(plugin Plugin) error {
|
func (m *Manager) Uninstall(plugin Plugin) error {
|
||||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
pluginPath, err := m.findInstalledPath(plugin.ID)
|
||||||
|
|
||||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
return fmt.Errorf("failed to find plugin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if pluginPath == "" {
|
||||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
|
||||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
|
||||||
}
|
|
||||||
if systemExists {
|
|
||||||
return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
|
||||||
|
return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
metaPath := pluginPath + ".meta"
|
metaPath := pluginPath + ".meta"
|
||||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -369,47 +395,174 @@ func (m *Manager) ListInstalled() ([]string, error) {
|
|||||||
|
|
||||||
// getPluginID reads the plugin.json file and returns the plugin ID
|
// getPluginID reads the plugin.json file and returns the plugin ID
|
||||||
func (m *Manager) getPluginID(pluginPath string) string {
|
func (m *Manager) getPluginID(pluginPath string) string {
|
||||||
|
manifest := m.getPluginManifest(pluginPath)
|
||||||
|
if manifest == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return manifest.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getPluginManifest(pluginPath string) *pluginManifest {
|
||||||
manifestPath := filepath.Join(pluginPath, "plugin.json")
|
manifestPath := filepath.Join(pluginPath, "plugin.json")
|
||||||
data, err := afero.ReadFile(m.fs, manifestPath)
|
data, err := afero.ReadFile(m.fs, manifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifest struct {
|
var manifest pluginManifest
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||||
return ""
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return manifest.ID
|
return &manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginManifest struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetPluginsDir() string {
|
func (m *Manager) GetPluginsDir() string {
|
||||||
return m.pluginsDir
|
return m.pluginsDir
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
|
func (m *Manager) UninstallByIDOrName(idOrName string) error {
|
||||||
pluginPath := filepath.Join(m.pluginsDir, pluginID)
|
pluginPath, err := m.findInstalledPathByIDOrName(idOrName)
|
||||||
|
|
||||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to check if plugin exists: %w", err)
|
return err
|
||||||
|
}
|
||||||
|
if pluginPath == "" {
|
||||||
|
return fmt.Errorf("plugin not found: %s", idOrName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
|
||||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", pluginID)
|
return fmt.Errorf("cannot uninstall system plugin: %s", idOrName)
|
||||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
}
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to check system plugin: %w", err)
|
metaPath := pluginPath + ".meta"
|
||||||
|
metaExists, _ := afero.Exists(m.fs, metaPath)
|
||||||
|
|
||||||
|
if metaExists {
|
||||||
|
if err := m.fs.Remove(pluginPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove symlink: %w", err)
|
||||||
}
|
}
|
||||||
if systemExists {
|
if err := m.fs.Remove(metaPath); err != nil {
|
||||||
return false, nil
|
return fmt.Errorf("failed to remove metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if err := m.fs.RemoveAll(pluginPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove plugin: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UpdateByIDOrName(idOrName string) error {
|
||||||
|
pluginPath, err := m.findInstalledPathByIDOrName(idOrName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pluginPath == "" {
|
||||||
|
return fmt.Errorf("plugin not found: %s", idOrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
|
||||||
|
return fmt.Errorf("cannot update system plugin: %s", idOrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
metaPath := pluginPath + ".meta"
|
||||||
|
metaExists, _ := afero.Exists(m.fs, metaPath)
|
||||||
|
|
||||||
|
if metaExists {
|
||||||
|
// Plugin is from monorepo, but we don't know the repo URL without registry
|
||||||
|
// Just try to pull from existing .git in the symlink target
|
||||||
|
return fmt.Errorf("cannot update monorepo plugin without registry info: %s", idOrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standalone plugin - just pull
|
||||||
|
if err := m.gitClient.Pull(pluginPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to update plugin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) findInstalledPathByIDOrName(idOrName string) (string, error) {
|
||||||
|
path, err := m.findInDirByIDOrName(m.pluginsDir, idOrName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
systemDir := "/etc/xdg/quickshell/dms-plugins"
|
||||||
|
return m.findInDirByIDOrName(systemDir, idOrName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) findInDirByIDOrName(dir, idOrName string) (string, error) {
|
||||||
|
// Check exact folder name match first
|
||||||
|
exactPath := filepath.Join(dir, idOrName)
|
||||||
|
if exists, _ := afero.DirExists(m.fs, exactPath); exists {
|
||||||
|
return exactPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := afero.DirExists(m.fs, dir)
|
||||||
|
if err != nil || !exists {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := afero.ReadDir(m.fs, dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if name == ".repos" || strings.HasSuffix(name, ".meta") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(dir, name)
|
||||||
|
isPlugin := entry.IsDir() || entry.Mode()&os.ModeSymlink != 0
|
||||||
|
if !isPlugin {
|
||||||
|
if info, err := m.fs.Stat(fullPath); err == nil && info.IsDir() {
|
||||||
|
isPlugin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isPlugin {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := m.getPluginManifest(fullPath)
|
||||||
|
if manifest == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if manifest.ID == idOrName || manifest.Name == idOrName {
|
||||||
|
return fullPath, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
|
||||||
|
pluginPath, err := m.findInstalledPath(pluginID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to find plugin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pluginPath == "" {
|
||||||
return false, fmt.Errorf("plugin not installed: %s", pluginID)
|
return false, fmt.Errorf("plugin not installed: %s", pluginID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there's a .meta file (plugin installed from a monorepo)
|
if strings.HasPrefix(pluginPath, "/etc/xdg/quickshell/dms-plugins") {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
metaPath := pluginPath + ".meta"
|
metaPath := pluginPath + ".meta"
|
||||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package plugins
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FuzzySearch(query string, plugins []Plugin) []Plugin {
|
func FuzzySearch(query string, plugins []Plugin) []Plugin {
|
||||||
@@ -11,18 +13,12 @@ func FuzzySearch(query string, plugins []Plugin) []Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queryLower := strings.ToLower(query)
|
queryLower := strings.ToLower(query)
|
||||||
var results []Plugin
|
return utils.Filter(plugins, func(p Plugin) bool {
|
||||||
|
return fuzzyMatch(queryLower, strings.ToLower(p.Name)) ||
|
||||||
for _, plugin := range plugins {
|
fuzzyMatch(queryLower, strings.ToLower(p.Category)) ||
|
||||||
if fuzzyMatch(queryLower, strings.ToLower(plugin.Name)) ||
|
fuzzyMatch(queryLower, strings.ToLower(p.Description)) ||
|
||||||
fuzzyMatch(queryLower, strings.ToLower(plugin.Category)) ||
|
fuzzyMatch(queryLower, strings.ToLower(p.Author))
|
||||||
fuzzyMatch(queryLower, strings.ToLower(plugin.Description)) ||
|
})
|
||||||
fuzzyMatch(queryLower, strings.ToLower(plugin.Author)) {
|
|
||||||
results = append(results, plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fuzzyMatch(query, text string) bool {
|
func fuzzyMatch(query, text string) bool {
|
||||||
@@ -39,57 +35,34 @@ func FilterByCategory(category string, plugins []Plugin) []Plugin {
|
|||||||
if category == "" {
|
if category == "" {
|
||||||
return plugins
|
return plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []Plugin
|
|
||||||
categoryLower := strings.ToLower(category)
|
categoryLower := strings.ToLower(category)
|
||||||
|
return utils.Filter(plugins, func(p Plugin) bool {
|
||||||
for _, plugin := range plugins {
|
return strings.ToLower(p.Category) == categoryLower
|
||||||
if strings.ToLower(plugin.Category) == categoryLower {
|
})
|
||||||
results = append(results, plugin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FilterByCompositor(compositor string, plugins []Plugin) []Plugin {
|
func FilterByCompositor(compositor string, plugins []Plugin) []Plugin {
|
||||||
if compositor == "" {
|
if compositor == "" {
|
||||||
return plugins
|
return plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []Plugin
|
|
||||||
compositorLower := strings.ToLower(compositor)
|
compositorLower := strings.ToLower(compositor)
|
||||||
|
return utils.Filter(plugins, func(p Plugin) bool {
|
||||||
for _, plugin := range plugins {
|
return utils.Any(p.Compositors, func(c string) bool {
|
||||||
for _, comp := range plugin.Compositors {
|
return strings.ToLower(c) == compositorLower
|
||||||
if strings.ToLower(comp) == compositorLower {
|
})
|
||||||
results = append(results, plugin)
|
})
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FilterByCapability(capability string, plugins []Plugin) []Plugin {
|
func FilterByCapability(capability string, plugins []Plugin) []Plugin {
|
||||||
if capability == "" {
|
if capability == "" {
|
||||||
return plugins
|
return plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []Plugin
|
|
||||||
capabilityLower := strings.ToLower(capability)
|
capabilityLower := strings.ToLower(capability)
|
||||||
|
return utils.Filter(plugins, func(p Plugin) bool {
|
||||||
for _, plugin := range plugins {
|
return utils.Any(p.Capabilities, func(c string) bool {
|
||||||
for _, cap := range plugin.Capabilities {
|
return strings.ToLower(c) == capabilityLower
|
||||||
if strings.ToLower(cap) == capabilityLower {
|
})
|
||||||
results = append(results, plugin)
|
})
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SortByFirstParty(plugins []Plugin) []Plugin {
|
func SortByFirstParty(plugins []Plugin) []Plugin {
|
||||||
@@ -103,3 +76,13 @@ func SortByFirstParty(plugins []Plugin) []Plugin {
|
|||||||
})
|
})
|
||||||
return plugins
|
return plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindByIDOrName(idOrName string, plugins []Plugin) *Plugin {
|
||||||
|
if p, found := utils.Find(plugins, func(p Plugin) bool { return p.ID == idOrName }); found {
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
if p, found := utils.Find(plugins, func(p Plugin) bool { return p.Name == idOrName }); found {
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func NewZdwlIpcManagerV2(ctx *client.Context) *ZdwlIpcManagerV2 {
|
|||||||
// Indicates that the client will not the dwl_ipc_manager object anymore.
|
// Indicates that the client will not the dwl_ipc_manager object anymore.
|
||||||
// Objects created through this instance are not affected.
|
// Objects created through this instance are not affected.
|
||||||
func (i *ZdwlIpcManagerV2) Release() error {
|
func (i *ZdwlIpcManagerV2) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -188,7 +188,7 @@ func NewZdwlIpcOutputV2(ctx *client.Context) *ZdwlIpcOutputV2 {
|
|||||||
//
|
//
|
||||||
// Indicates to that the client no longer needs this dwl_ipc_output.
|
// Indicates to that the client no longer needs this dwl_ipc_output.
|
||||||
func (i *ZdwlIpcOutputV2) Release() error {
|
func (i *ZdwlIpcOutputV2) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func (i *ExtWorkspaceManagerV1) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ExtWorkspaceManagerV1) Destroy() error {
|
func (i *ExtWorkspaceManagerV1) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ func (i *ExtWorkspaceGroupHandleV1) CreateWorkspace(workspace string) error {
|
|||||||
// use the workspace group object any more or after the removed event to finalize
|
// use the workspace group object any more or after the removed event to finalize
|
||||||
// the destruction of the object.
|
// the destruction of the object.
|
||||||
func (i *ExtWorkspaceGroupHandleV1) Destroy() error {
|
func (i *ExtWorkspaceGroupHandleV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -655,7 +655,7 @@ func NewExtWorkspaceHandleV1(ctx *client.Context) *ExtWorkspaceHandleV1 {
|
|||||||
// use the workspace object any more or after the remove event to finalize
|
// use the workspace object any more or after the remove event to finalize
|
||||||
// the destruction of the object.
|
// the destruction of the object.
|
||||||
func (i *ExtWorkspaceHandleV1) Destroy() error {
|
func (i *ExtWorkspaceHandleV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
@@ -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.MarkZombie()
|
||||||
|
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.MarkZombie()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ func (i *ZwlrGammaControlManagerV1) GetGammaControl(output *client.Output) (*Zwl
|
|||||||
// All objects created by the manager will still remain valid, until their
|
// All objects created by the manager will still remain valid, until their
|
||||||
// appropriate destroy request has been called.
|
// appropriate destroy request has been called.
|
||||||
func (i *ZwlrGammaControlManagerV1) Destroy() error {
|
func (i *ZwlrGammaControlManagerV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -169,7 +169,7 @@ func (i *ZwlrGammaControlV1) SetGamma(fd int) error {
|
|||||||
// Destroys the gamma control object. If the object is still valid, this
|
// Destroys the gamma control object. If the object is still valid, this
|
||||||
// restores the original gamma tables.
|
// restores the original gamma tables.
|
||||||
func (i *ZwlrGammaControlV1) Destroy() error {
|
func (i *ZwlrGammaControlV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
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.MarkZombie()
|
||||||
|
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.MarkZombie()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -172,7 +172,7 @@ func (i *ZwlrOutputManagerV1) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ZwlrOutputManagerV1) Destroy() error {
|
func (i *ZwlrOutputManagerV1) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,9 +238,17 @@ func (i *ZwlrOutputManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|||||||
l := 0
|
l := 0
|
||||||
objectID := client.Uint32(data[l : l+4])
|
objectID := client.Uint32(data[l : l+4])
|
||||||
proxy := i.Context().GetProxy(objectID)
|
proxy := i.Context().GetProxy(objectID)
|
||||||
if proxy != nil {
|
if proxy == nil {
|
||||||
e.Head = proxy.(*ZwlrOutputHeadV1)
|
head := &ZwlrOutputHeadV1{}
|
||||||
|
head.SetContext(i.Context())
|
||||||
|
head.SetID(objectID)
|
||||||
|
registerServerProxy(i.Context(), head, objectID)
|
||||||
|
e.Head = head
|
||||||
|
} else if head, ok := proxy.(*ZwlrOutputHeadV1); ok {
|
||||||
|
e.Head = head
|
||||||
} else {
|
} else {
|
||||||
|
// Stale proxy of wrong type (can happen after suspend/resume)
|
||||||
|
// Replace it with the correct type
|
||||||
head := &ZwlrOutputHeadV1{}
|
head := &ZwlrOutputHeadV1{}
|
||||||
head.SetContext(i.Context())
|
head.SetContext(i.Context())
|
||||||
head.SetID(objectID)
|
head.SetID(objectID)
|
||||||
@@ -334,7 +342,7 @@ func NewZwlrOutputHeadV1(ctx *client.Context) *ZwlrOutputHeadV1 {
|
|||||||
// This request indicates that the client will no longer use this head
|
// This request indicates that the client will no longer use this head
|
||||||
// object.
|
// object.
|
||||||
func (i *ZwlrOutputHeadV1) Release() error {
|
func (i *ZwlrOutputHeadV1) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -715,9 +723,17 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|||||||
l := 0
|
l := 0
|
||||||
objectID := client.Uint32(data[l : l+4])
|
objectID := client.Uint32(data[l : l+4])
|
||||||
proxy := i.Context().GetProxy(objectID)
|
proxy := i.Context().GetProxy(objectID)
|
||||||
if proxy != nil {
|
if proxy == nil {
|
||||||
e.Mode = proxy.(*ZwlrOutputModeV1)
|
mode := &ZwlrOutputModeV1{}
|
||||||
|
mode.SetContext(i.Context())
|
||||||
|
mode.SetID(objectID)
|
||||||
|
registerServerProxy(i.Context(), mode, objectID)
|
||||||
|
e.Mode = mode
|
||||||
|
} else if mode, ok := proxy.(*ZwlrOutputModeV1); ok {
|
||||||
|
e.Mode = mode
|
||||||
} else {
|
} else {
|
||||||
|
// Stale proxy of wrong type (can happen after suspend/resume)
|
||||||
|
// Replace it with the correct type
|
||||||
mode := &ZwlrOutputModeV1{}
|
mode := &ZwlrOutputModeV1{}
|
||||||
mode.SetContext(i.Context())
|
mode.SetContext(i.Context())
|
||||||
mode.SetID(objectID)
|
mode.SetID(objectID)
|
||||||
@@ -743,7 +759,26 @@ func (i *ZwlrOutputHeadV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|||||||
}
|
}
|
||||||
var e ZwlrOutputHeadV1CurrentModeEvent
|
var e ZwlrOutputHeadV1CurrentModeEvent
|
||||||
l := 0
|
l := 0
|
||||||
e.Mode = i.Context().GetProxy(client.Uint32(data[l : l+4])).(*ZwlrOutputModeV1)
|
objectID := client.Uint32(data[l : l+4])
|
||||||
|
proxy := i.Context().GetProxy(objectID)
|
||||||
|
if proxy == nil {
|
||||||
|
// Mode not yet registered, create it
|
||||||
|
mode := &ZwlrOutputModeV1{}
|
||||||
|
mode.SetContext(i.Context())
|
||||||
|
mode.SetID(objectID)
|
||||||
|
registerServerProxy(i.Context(), mode, objectID)
|
||||||
|
e.Mode = mode
|
||||||
|
} else if mode, ok := proxy.(*ZwlrOutputModeV1); ok {
|
||||||
|
e.Mode = mode
|
||||||
|
} else {
|
||||||
|
// Stale proxy of wrong type (can happen after suspend/resume)
|
||||||
|
// Replace it with the correct type
|
||||||
|
mode := &ZwlrOutputModeV1{}
|
||||||
|
mode.SetContext(i.Context())
|
||||||
|
mode.SetID(objectID)
|
||||||
|
registerServerProxy(i.Context(), mode, objectID)
|
||||||
|
e.Mode = mode
|
||||||
|
}
|
||||||
l += 4
|
l += 4
|
||||||
|
|
||||||
i.currentModeHandler(e)
|
i.currentModeHandler(e)
|
||||||
@@ -879,7 +914,7 @@ func NewZwlrOutputModeV1(ctx *client.Context) *ZwlrOutputModeV1 {
|
|||||||
// This request indicates that the client will no longer use this mode
|
// This request indicates that the client will no longer use this mode
|
||||||
// object.
|
// object.
|
||||||
func (i *ZwlrOutputModeV1) Release() error {
|
func (i *ZwlrOutputModeV1) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -1132,7 +1167,7 @@ func (i *ZwlrOutputConfigurationV1) Test() error {
|
|||||||
// This request also destroys wlr_output_configuration_head objects created
|
// This request also destroys wlr_output_configuration_head objects created
|
||||||
// via this object.
|
// via this object.
|
||||||
func (i *ZwlrOutputConfigurationV1) Destroy() error {
|
func (i *ZwlrOutputConfigurationV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 4
|
const opcode = 4
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -1415,7 +1450,7 @@ func (i *ZwlrOutputConfigurationHeadV1) SetAdaptiveSync(state uint32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ZwlrOutputConfigurationHeadV1) Destroy() error {
|
func (i *ZwlrOutputConfigurationHeadV1) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (i *ZwlrOutputPowerManagerV1) GetOutputPower(output *client.Output) (*ZwlrO
|
|||||||
// All objects created by the manager will still remain valid, until their
|
// All objects created by the manager will still remain valid, until their
|
||||||
// appropriate destroy request has been called.
|
// appropriate destroy request has been called.
|
||||||
func (i *ZwlrOutputPowerManagerV1) Destroy() error {
|
func (i *ZwlrOutputPowerManagerV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -143,7 +143,7 @@ func (i *ZwlrOutputPowerV1) SetMode(mode uint32) error {
|
|||||||
//
|
//
|
||||||
// Destroys the output power management mode control object.
|
// Destroys the output power management mode control object.
|
||||||
func (i *ZwlrOutputPowerV1) Destroy() error {
|
func (i *ZwlrOutputPowerV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
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.MarkZombie()
|
||||||
|
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.MarkZombie()
|
||||||
|
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.MarkZombie()
|
||||||
|
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.MarkZombie()
|
||||||
|
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>
|
||||||
651
core/internal/screenshot/compositor.go
Normal file
651
core/internal/screenshot/compositor.go
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Compositor int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompositorUnknown Compositor = iota
|
||||||
|
CompositorHyprland
|
||||||
|
CompositorSway
|
||||||
|
CompositorNiri
|
||||||
|
CompositorDWL
|
||||||
|
CompositorScroll
|
||||||
|
)
|
||||||
|
|
||||||
|
var detectedCompositor Compositor = -1
|
||||||
|
|
||||||
|
func DetectCompositor() Compositor {
|
||||||
|
if detectedCompositor >= 0 {
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
hyprlandSig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
||||||
|
niriSocket := os.Getenv("NIRI_SOCKET")
|
||||||
|
swaySocket := os.Getenv("SWAYSOCK")
|
||||||
|
scrollSocket := os.Getenv("SCROLLSOCK")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case niriSocket != "":
|
||||||
|
if _, err := os.Stat(niriSocket); err == nil {
|
||||||
|
detectedCompositor = CompositorNiri
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
case scrollSocket != "":
|
||||||
|
if _, err := os.Stat(scrollSocket); err == nil {
|
||||||
|
detectedCompositor = CompositorScroll
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
case swaySocket != "":
|
||||||
|
if _, err := os.Stat(swaySocket); err == nil {
|
||||||
|
detectedCompositor = CompositorSway
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
case hyprlandSig != "":
|
||||||
|
detectedCompositor = CompositorHyprland
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectDWLProtocol() {
|
||||||
|
detectedCompositor = CompositorDWL
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
detectedCompositor = CompositorUnknown
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectDWLProtocol() bool {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
if e.Interface == dwl_ipc.ZdwlIpcManagerV2InterfaceName {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetCompositorDWL() {
|
||||||
|
detectedCompositor = CompositorDWL
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowGeometry struct {
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
Width int32
|
||||||
|
Height int32
|
||||||
|
Output string
|
||||||
|
Scale float64
|
||||||
|
OutputX int32
|
||||||
|
OutputY int32
|
||||||
|
OutputTransform int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetActiveWindow() (*WindowGeometry, error) {
|
||||||
|
switch DetectCompositor() {
|
||||||
|
case CompositorHyprland:
|
||||||
|
return getHyprlandActiveWindow()
|
||||||
|
case CompositorDWL:
|
||||||
|
return getDWLActiveWindow()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("window capture requires Hyprland or DWL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hyprlandWindow struct {
|
||||||
|
At [2]int32 `json:"at"`
|
||||||
|
Size [2]int32 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHyprlandActiveWindow() (*WindowGeometry, error) {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "activewindow").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
|
||||||
|
}
|
||||||
|
|
||||||
|
type hyprlandMonitor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
X int32 `json:"x"`
|
||||||
|
Y int32 `json:"y"`
|
||||||
|
Width int32 `json:"width"`
|
||||||
|
Height int32 `json:"height"`
|
||||||
|
Scale float64 `json:"scale"`
|
||||||
|
Focused bool `json:"focused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHyprlandMonitorScale(name string) float64 {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitors []hyprlandMonitor
|
||||||
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range monitors {
|
||||||
|
if m.Name == name {
|
||||||
|
return m.Scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHyprlandFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitors []hyprlandMonitor
|
||||||
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range monitors {
|
||||||
|
if m.Focused {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHyprlandMonitorGeometry(name string) (x, y, w, h int32, ok bool) {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitors []hyprlandMonitor
|
||||||
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
||||||
|
return 0, 0, 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range monitors {
|
||||||
|
if m.Name == name {
|
||||||
|
logicalW := int32(float64(m.Width) / m.Scale)
|
||||||
|
logicalH := int32(float64(m.Height) / m.Scale)
|
||||||
|
return m.X, m.Y, logicalW, logicalH, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0, 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type swayWorkspace struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
Focused bool `json:"focused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSwayFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("swaymsg", "-t", "get_workspaces").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var workspaces []swayWorkspace
|
||||||
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
if ws.Focused {
|
||||||
|
return ws.Output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScrollFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("scrollmsg", "-t", "get_workspaces").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var workspaces []swayWorkspace
|
||||||
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
if ws.Focused {
|
||||||
|
return ws.Output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type niriWorkspace struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
IsFocused bool `json:"is_focused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNiriFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("niri", "msg", "-j", "workspaces").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var workspaces []niriWorkspace
|
||||||
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
if ws.IsFocused {
|
||||||
|
return ws.Output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlActiveOutput string
|
||||||
|
|
||||||
|
func SetDWLActiveOutput(name string) {
|
||||||
|
dwlActiveOutput = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDWLFocusedMonitor() string {
|
||||||
|
if dwlActiveOutput != "" {
|
||||||
|
return dwlActiveOutput
|
||||||
|
}
|
||||||
|
return queryDWLActiveOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryDWLActiveOutput() string {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
|
||||||
|
outputs := make(map[uint32]*client.Output)
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
|
||||||
|
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
dwlManager = mgr
|
||||||
|
}
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
out := client.NewOutput(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
|
||||||
|
outputs[e.Name] = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if dwlManager == nil || len(outputs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
outputNames := make(map[uint32]string)
|
||||||
|
for name, out := range outputs {
|
||||||
|
n := name
|
||||||
|
out.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
outputNames[n] = e.Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type outputState struct {
|
||||||
|
name string
|
||||||
|
active bool
|
||||||
|
gotFrame bool
|
||||||
|
}
|
||||||
|
states := make(map[uint32]*outputState)
|
||||||
|
|
||||||
|
for name, out := range outputs {
|
||||||
|
dwlOut, err := dwlManager.GetOutput(out)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state := &outputState{name: outputNames[name]}
|
||||||
|
states[name] = state
|
||||||
|
|
||||||
|
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
|
||||||
|
state.active = e.Active != 0
|
||||||
|
})
|
||||||
|
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
|
||||||
|
state.gotFrame = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
allFramesReceived := func() bool {
|
||||||
|
for _, s := range states {
|
||||||
|
if !s.gotFrame {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for !allFramesReceived() {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range states {
|
||||||
|
if state.active {
|
||||||
|
return state.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFocusedMonitor() string {
|
||||||
|
switch DetectCompositor() {
|
||||||
|
case CompositorHyprland:
|
||||||
|
return getHyprlandFocusedMonitor()
|
||||||
|
case CompositorSway:
|
||||||
|
return getSwayFocusedMonitor()
|
||||||
|
case CompositorScroll:
|
||||||
|
return getScrollFocusedMonitor()
|
||||||
|
case CompositorNiri:
|
||||||
|
return getNiriFocusedMonitor()
|
||||||
|
case CompositorDWL:
|
||||||
|
return getDWLFocusedMonitor()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type outputInfo struct {
|
||||||
|
x, y int32
|
||||||
|
transform int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
||||||
|
mgr := wlr_output_management.NewZwlrOutputManagerV1(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
||||||
|
outputManager = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputManager == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type headState struct {
|
||||||
|
name string
|
||||||
|
x, y int32
|
||||||
|
transform int32
|
||||||
|
}
|
||||||
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
|
done := false
|
||||||
|
|
||||||
|
outputManager.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||||
|
state := &headState{}
|
||||||
|
heads[e.Head] = state
|
||||||
|
e.Head.SetNameHandler(func(ne wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||||
|
state.name = ne.Name
|
||||||
|
})
|
||||||
|
e.Head.SetPositionHandler(func(pe wlr_output_management.ZwlrOutputHeadV1PositionEvent) {
|
||||||
|
state.x = pe.X
|
||||||
|
state.y = pe.Y
|
||||||
|
})
|
||||||
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
|
state.transform = te.Transform
|
||||||
|
})
|
||||||
|
})
|
||||||
|
outputManager.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
||||||
|
done = true
|
||||||
|
})
|
||||||
|
|
||||||
|
for !done {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range heads {
|
||||||
|
if state.name == outputName {
|
||||||
|
return &outputInfo{
|
||||||
|
x: state.x,
|
||||||
|
y: state.y,
|
||||||
|
transform: state.transform,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect: %w", err)
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
|
||||||
|
outputs := make(map[uint32]*client.Output)
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
|
||||||
|
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
dwlManager = mgr
|
||||||
|
}
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
out := client.NewOutput(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
|
||||||
|
outputs[e.Name] = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dwlManager == nil {
|
||||||
|
return nil, fmt.Errorf("dwl_ipc_manager not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outputs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no outputs found")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputNames := make(map[uint32]string)
|
||||||
|
for name, out := range outputs {
|
||||||
|
n := name
|
||||||
|
out.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
outputNames[n] = e.Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dwlOutputState struct {
|
||||||
|
output *dwl_ipc.ZdwlIpcOutputV2
|
||||||
|
name string
|
||||||
|
active bool
|
||||||
|
x, y int32
|
||||||
|
w, h int32
|
||||||
|
scalefactor uint32
|
||||||
|
gotFrame bool
|
||||||
|
}
|
||||||
|
|
||||||
|
dwlOutputs := make(map[uint32]*dwlOutputState)
|
||||||
|
for name, out := range outputs {
|
||||||
|
dwlOut, err := dwlManager.GetOutput(out)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state := &dwlOutputState{output: dwlOut, name: outputNames[name]}
|
||||||
|
dwlOutputs[name] = state
|
||||||
|
|
||||||
|
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
|
||||||
|
state.active = e.Active != 0
|
||||||
|
})
|
||||||
|
dwlOut.SetXHandler(func(e dwl_ipc.ZdwlIpcOutputV2XEvent) {
|
||||||
|
state.x = e.X
|
||||||
|
})
|
||||||
|
dwlOut.SetYHandler(func(e dwl_ipc.ZdwlIpcOutputV2YEvent) {
|
||||||
|
state.y = e.Y
|
||||||
|
})
|
||||||
|
dwlOut.SetWidthHandler(func(e dwl_ipc.ZdwlIpcOutputV2WidthEvent) {
|
||||||
|
state.w = e.Width
|
||||||
|
})
|
||||||
|
dwlOut.SetHeightHandler(func(e dwl_ipc.ZdwlIpcOutputV2HeightEvent) {
|
||||||
|
state.h = e.Height
|
||||||
|
})
|
||||||
|
dwlOut.SetScalefactorHandler(func(e dwl_ipc.ZdwlIpcOutputV2ScalefactorEvent) {
|
||||||
|
state.scalefactor = e.Scalefactor
|
||||||
|
})
|
||||||
|
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
|
||||||
|
state.gotFrame = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
allFramesReceived := func() bool {
|
||||||
|
for _, s := range dwlOutputs {
|
||||||
|
if !s.gotFrame {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for !allFramesReceived() {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil, fmt.Errorf("dispatch: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range dwlOutputs {
|
||||||
|
if !state.active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if state.w <= 0 || state.h <= 0 {
|
||||||
|
return nil, fmt.Errorf("no active window")
|
||||||
|
}
|
||||||
|
scale := float64(state.scalefactor) / 100.0
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
geom := &WindowGeometry{
|
||||||
|
X: state.x,
|
||||||
|
Y: state.y,
|
||||||
|
Width: state.w,
|
||||||
|
Height: state.h,
|
||||||
|
Output: state.name,
|
||||||
|
Scale: scale,
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, ok := getOutputInfo(state.name); ok {
|
||||||
|
geom.OutputX = info.x
|
||||||
|
geom.OutputY = info.y
|
||||||
|
geom.OutputTransform = info.transform
|
||||||
|
}
|
||||||
|
|
||||||
|
return geom, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no active output found")
|
||||||
|
}
|
||||||
166
core/internal/screenshot/encode.go
Normal file
166
core/internal/screenshot/encode.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
var swapRB bool
|
||||||
|
switch format {
|
||||||
|
case uint32(FormatABGR8888), uint32(FormatXBGR8888):
|
||||||
|
swapRB = false
|
||||||
|
default:
|
||||||
|
swapRB = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
userDirsFile := filepath.Join(utils.XDGConfigHome(), "user-dirs.dirs")
|
||||||
|
data, err := os.ReadFile(userDirsFile)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
if len(line) == 0 || line[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prefix = "XDG_PICTURES_DIR="
|
||||||
|
if !strings.HasPrefix(line, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := strings.Trim(line[len(prefix):], "\"")
|
||||||
|
expanded, err := utils.ExpandPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return expanded
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
855
core/internal/screenshot/region.go
Normal file
855
core/internal/screenshot/region.go
Normal file
@@ -0,0 +1,855 @@
|
|||||||
|
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,
|
||||||
|
fractionalScale: 1.0,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
o.fractionalScale = float64(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
|
||||||
|
}
|
||||||
|
|
||||||
|
var capturedBuf *ShmBuffer
|
||||||
|
|
||||||
|
var capturedFormat PixelFormat
|
||||||
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
capturedFormat = PixelFormat(e.Format)
|
||||||
|
bpp := capturedFormat.BytesPerPixel()
|
||||||
|
if int(e.Stride) < int(e.Width)*bpp {
|
||||||
|
log.Error("invalid stride from compositor", "stride", e.Stride, "width", e.Width, "bpp", bpp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create screen buffer failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
capturedBuf = buf
|
||||||
|
buf.Format = capturedFormat
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if capturedBuf == nil {
|
||||||
|
onReady()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if capturedFormat.Is24Bit() {
|
||||||
|
converted, newFormat, err := capturedBuf.ConvertTo32Bit(capturedFormat)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("convert 24-bit to 32-bit failed", "err", err)
|
||||||
|
} else if converted != capturedBuf {
|
||||||
|
capturedBuf.Close()
|
||||||
|
capturedBuf = converted
|
||||||
|
capturedFormat = newFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.format = uint32(capturedFormat)
|
||||||
|
|
||||||
|
if pc.yInverted {
|
||||||
|
capturedBuf.FlipVertical()
|
||||||
|
pc.yInverted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if output.transform != TransformNormal {
|
||||||
|
invTransform := InverseTransform(output.transform)
|
||||||
|
transformed, err := capturedBuf.ApplyTransform(invTransform)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("apply transform failed", "err", err)
|
||||||
|
} else if transformed != capturedBuf {
|
||||||
|
capturedBuf.Close()
|
||||||
|
capturedBuf = transformed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if withCursor {
|
||||||
|
pc.screenBuf = capturedBuf
|
||||||
|
} else {
|
||||||
|
pc.screenBufNoCursor = capturedBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if os.logicalW > 0 && os.screenBuf != nil {
|
||||||
|
os.output.fractionalScale = float64(os.screenBuf.Width) / float64(os.logicalW)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if os.viewport != nil {
|
||||||
|
_ = os.wlSurface.SetBufferScale(1)
|
||||||
|
_ = os.viewport.SetSource(0, 0, float64(slot.shm.Width), float64(slot.shm.Height))
|
||||||
|
_ = os.viewport.SetDestination(int32(os.logicalW), int32(os.logicalH))
|
||||||
|
} else {
|
||||||
|
bufferScale := os.output.scale
|
||||||
|
if bufferScale <= 0 {
|
||||||
|
bufferScale = 1
|
||||||
|
}
|
||||||
|
_ = os.wlSurface.SetBufferScale(bufferScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
271
core/internal/screenshot/region_input.go
Normal file
271
core/internal/screenshot/region_input.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
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 os.yInverted {
|
||||||
|
srcY = srcBuf.Height - 1 - (by1 + y)
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= srcBuf.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dstY := y
|
||||||
|
if os.yInverted {
|
||||||
|
dstY = h - 1 - y
|
||||||
|
}
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
srcX := bx1 + x
|
||||||
|
if srcX < 0 || srcX >= srcBuf.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
si := srcY*srcBuf.Stride + srcX*4
|
||||||
|
di := dstY*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
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user