1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

Compare commits

..

95 Commits

Author SHA1 Message Date
purian23
8ce78e7134 Dependency removals from Dankinstaller Distros
- Removed grim, grimblast, slurp, hyprpicker & mate-polkit from all distros
2025-12-06 01:10:13 -05:00
Yuxiang Qiu
9ebfab2e78 brightness: rescan brightness (#922) 2025-12-06 00:24:54 -05:00
bbedward
833d245251 dankbar: fix centersection positioning 2025-12-05 23:59:06 -05:00
bbedward
00d3024143 dankbar: keep border on maximize 2025-12-05 23:50:27 -05:00
bbedward
aedeab8a6a screenshot: add window capture for Hyprland 2025-12-05 21:10:12 -05:00
Pi Home Server
4d39169eb8 Feature/control center widget fix (#912)
* Add a widget to display the power menu

* Update power button widget

* Upate based on new settings

* Rollback to DisplaysTab.qml
2025-12-05 20:29:39 -05:00
bbedward
2ddc448150 screenshot: ensure screencopy before surface creation 2025-12-05 17:39:35 -05:00
bbedward
f9a6b4ce2c colorpick/screenshot: make color-format aware 2025-12-05 17:26:38 -05:00
bbedward
22b2b69413 screenshot: add shift to perfect-square capability 2025-12-05 17:08:00 -05:00
bbedward
7f11632ea6 screenshot: fix notif content to show open file browser 2025-12-05 16:56:29 -05:00
bbedward
c0b4d5e2c2 screenshot: fix thumbnail preview 2025-12-05 16:16:13 -05:00
Lucas
2c23d0249c nix: match upstream package format (#918) 2025-12-05 16:11:18 -05:00
bbedward
c3233fbf61 power menu: shorter hold durations 2025-12-05 16:05:11 -05:00
bbedward
ecfc8e208c screenshot: clipboard by default 2025-12-05 15:59:37 -05:00
bbedward
52d5e21fc4 screenshot: fix some region mappings 2025-12-05 15:25:27 -05:00
bbedward
6d0c56554f core: add screenshot utility 2025-12-05 14:59:34 -05:00
bbedward
844e91dc9e controlcenter: default vpn button to on 2025-12-05 14:21:19 -05:00
bbedward
1f00b5f577 fix some stale screen ref issues in OSD and popout 2025-12-05 13:31:57 -05:00
bbedward
2c48458384 brightness: more aggressive ddc rescans on device changes 2025-12-05 13:18:10 -05:00
bbedward
ddda87c5a7 less agress dms-open MimeType declarations 2025-12-05 12:36:04 -05:00
bbedward
6b1bbca620 keybinds: fix alt+shift, kdl parsing, allow arguments 2025-12-05 12:31:15 -05:00
bbedward
b5378e5d3c hypr: add exclusive focus override 2025-12-05 10:37:24 -05:00
bbedward
c69a55df29 flickable: update momentum scrolling logic 2025-12-05 10:14:16 -05:00
bbedward
5faa1a993a launcher: reemove background from list and add a bottom fade 2025-12-05 10:04:19 -05:00
bbedward
e56481f6d7 launcher: add 1px gap between grid delegates 2025-12-05 09:33:04 -05:00
bbedward
f9610d457c dankbar: fix border thickness 2025-12-05 09:29:45 -05:00
bbedward
ae066f42a4 brightness: delay screen change rescan of devices 2025-12-04 23:10:25 -05:00
bbedward
c60dd42fa7 dankinstall: set default niri config with includes 2025-12-04 22:45:46 -05:00
Yuxiang Qiu
7aac5ac5a1 dankbar: fix privacy indicator background color (#909) 2025-12-04 21:32:48 -05:00
bbedward
ad0f3fa33b dankbar: convert center section to use WidgetHost 2025-12-04 19:37:21 -05:00
bbedward
63d121b796 proc: ability to run command with noTimeout 2025-12-04 16:09:38 -05:00
bbedward
4291cfe82f settings: fix launcher tab sizing 2025-12-04 16:01:07 -05:00
bbedward
f312868154 lock: respect confirmation mode power actions 2025-12-04 14:58:36 -05:00
bbedward
5b42d34ac8 expose iconSize helpers to plugins 2025-12-04 14:40:38 -05:00
bbedward
397a8c275d settings: add IPCs to open specific settings tabs 2025-12-04 14:31:35 -05:00
purian23
2aabee453b Remove hyprpicker requirement for DMS Copr 2025-12-04 14:25:40 -05:00
bbedward
185333a615 brightness: default IPCs to pinned devices per-display
fixes #875
2025-12-04 13:55:14 -05:00
bbedward
7d177eb1d4 greeter: fix mango config override
fixes #904
2025-12-04 13:46:11 -05:00
Givani Boekestijn
705a84051d feat(dankdash): add vim keybindings (hjkl) to wallpaper picker navigation (#903) 2025-12-04 13:22:30 -05:00
bbedward
f6821f80e1 dankslideout: convert to Rectangle 2025-12-04 12:54:15 -05:00
bbedward
e7a6f5228d widgets: fix binding loop in button 2025-12-04 12:50:06 -05:00
bbedward
8161fd6acb i18n: add hebrew *partial*
- Most widgets and components lack proper RTL layout support
- Merging hebrew anyway, as these can be updated incrementally later
2025-12-04 11:39:58 -05:00
bbedward
2137920e81 dankslideout: put opacity on parent layer 2025-12-04 10:06:45 -05:00
bbedward
879102599c matugen: package vscode theme as vsix 2025-12-04 09:39:29 -05:00
bbedward
44190f07fe colorpicker: hide magnifier on startup 2025-12-04 09:14:12 -05:00
bbedward
a41487eb8f colorpicker: hide magnfier on monitor leave 2025-12-04 09:12:21 -05:00
bbedward
e1acaaa27c dankbar: add option to disable maximize detection
fixes #895
2025-12-04 08:56:04 -05:00
Marcus Ramberg
08a97aeff8 power: support automatic profile switching on battery change (#897) 2025-12-04 08:37:07 -05:00
bbedward
5b7302b46d color picker: use shortcuts inhibitor when active 2025-12-04 00:08:43 -05:00
purian23
34c0bba130 Add Debian / Ubuntu / OpenSuse support to DankInstaller 2025-12-03 23:41:17 -05:00
bbedward
5a53447272 color picker: switch to dms picker 2025-12-03 23:18:46 -05:00
bbedward
b6847289ff keybinds tab: change colors 2025-12-03 23:14:20 -05:00
bbedward
d22c43e08b app picker: fix background close 2025-12-03 23:00:22 -05:00
bbedward
d9deaa8d74 cli: add interactive color picker 2025-12-03 22:29:57 -05:00
bbedward
6c7776a9a6 audio: add IPC & OSD for changing output audio device
fixes #754
2025-12-03 20:47:57 -05:00
bbedward
62bd6e41ef settings: break out dank bar widgets 2025-12-03 18:17:06 -05:00
bbedward
293c7b42c6 pass screen to modals 2025-12-03 17:27:07 -05:00
bbedward
788da62777 settings: mecha re-organization 2025-12-03 17:25:40 -05:00
bbedward
2c7f24a913 lock: add option to show on 1 display
fixes #607
2025-12-03 12:15:22 -05:00
bbedward
f236706d6a hyprland: fix workspace overview truncation, update scaling
fixes #871
2025-12-03 12:02:41 -05:00
purian23
b097700591 Add dbus notifications inline to systemd 2025-12-03 11:53:31 -05:00
purian23
50b112c9d6 Revert "Add DMS dbus notification service file"
This reverts commit 33e655becd.
2025-12-03 11:48:56 -05:00
purian23
c2f478b088 Remove notification conflict 2025-12-03 11:16:04 -05:00
bbedward
dccbb137d7 launcher: integrate dsearch into drawer 2025-12-03 10:49:08 -05:00
bbedward
90f9940dbd gamma: fix night mode on startup 2025-12-03 10:37:28 -05:00
bbedward
f3f7cc9077 Revert "modals: single window optimization"
This reverts commit 468e569bc7.
2025-12-03 10:34:40 -05:00
bbedward
c331e2f39e Revert "spotlight: optimize to keep loaded"
This reverts commit 01b28e3ee8.
2025-12-03 10:34:19 -05:00
bbedward
1c7ebc4323 Revert "dankmodal: fix persistent modal handling"
This reverts commit e7cb0d397e.
2025-12-03 10:34:15 -05:00
bbedward
5f5427266f keybinds: always parse binds.kdl, show warning on position-conflicts 2025-12-03 10:32:16 -05:00
purian23
33e655becd Add DMS dbus notification service file 2025-12-03 09:49:34 -05:00
bbedward
0ea0602aec notif: fix keyboard navi in popout 2025-12-03 00:59:41 -05:00
bbedward
46effd2ca4 keybind: dont make shortcut inhbitor at compile time 2025-12-03 00:50:34 -05:00
bbedward
de055e8260 i18n: update terms 2025-12-03 00:34:31 -05:00
bbedward
c3077304af keybinds: move static arrays to js files 2025-12-03 00:21:11 -05:00
purian23
e15135911f DMS Version Formatting 2025-12-03 00:19:18 -05:00
purian23
d430cae944 fix: Duplicate build automation 2025-12-02 23:14:05 -05:00
bbedward
f92dc6f71b keyboard shortcuts: comprehensive keyboard shortcut management interface
- niri only for now
- requires quickshell-git, hidden otherwise
- Add, Edit, Delete keybinds
- Large suite of pre-defined and custom actions
- Works with niri 25.11+ include feature
2025-12-02 23:08:23 -05:00
purian23
a679be68b1 Update DMS versioning for Distro packages 2025-12-02 22:28:05 -05:00
bbedward
c5c5ce8409 i18n: add spanish 2025-12-02 21:18:45 -05:00
bbedward
e7cb0d397e dankmodal: fix persistent modal handling 2025-12-02 21:11:18 -05:00
Jon Rogers
b84308cb49 packaging: Add dms-open.desktop and danklogo.svg to all distribution packages (#870)
* packaging: add dms-open.desktop and danklogo.svg to all distributions

- Add dms-open.desktop to /usr/share/applications
- Add danklogo.svg to /usr/share/icons/hicolor/scalable/apps
- Updated packaging for:
  - Fedora (dms.spec)
  - OpenSUSE (dms.spec, dms-git.spec)
  - Debian (dms, dms-git)
  - Ubuntu (dms, dms-git)

Fixes #860

* nix: add dms-open.desktop and danklogo.svg to dankMaterialShell package

* Revert "packaging: add dms-open.desktop and danklogo.svg to all distributions"

This reverts commit 862a4fc405.

* nix: add dankMaterialShell to pkgs

---------

Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2025-12-02 22:32:59 -03:00
Marcus Ramberg
0df47d2ce3 core: add dynamic completion for more commands (#889) 2025-12-02 18:35:51 -05:00
purian23
e24b548b54 fix: dms-cli & about versioning in all builds 2025-12-02 18:12:13 -05:00
Lucas
75af444cee niri: add option to disable overview launcher (#887) 2025-12-02 18:04:04 -05:00
bbedward
02dd19962f matugen: backup and add to vscode extensions json when present 2025-12-02 17:32:48 -05:00
purian23
f552b8ef7b Update Debian version format 2025-12-02 16:51:58 -05:00
Marcus Ramberg
9162e31489 core: add dynamic completion for ipc command (#885) 2025-12-02 15:51:26 -05:00
bbedward
01b28e3ee8 spotlight: optimize to keep loaded 2025-12-02 15:01:23 -05:00
bbedward
f5aa855125 network: eth device speed is not exposed 2025-12-02 14:45:28 -05:00
Guilherme Pagano
db3610fcdb feat: add support for geometric centering (#856)
Introduces a configurable centering mode.
- Adds 'geometric' option.
- Retains 'index' as the default value to preserve existing behavior.
2025-12-02 14:43:51 -05:00
bbedward
2e3f330058 theme: uncomment niri alt-tab colors 2025-12-02 14:41:09 -05:00
Marcus Ramberg
1617a7f2c1 dankbar: allow disabling title scrolling in the music display (#882) 2025-12-02 13:39:19 -05:00
bbedward
69a5566bf9 dankbar: shrink to 0 spacing and no border when maximized surface is
present
2025-12-02 11:22:50 -05:00
Marcus Ramberg
30e5d8b855 core: fix crash on tui startup on nixos after removal of component detection (#881)
```sh
❯ dms
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0xb2fbe5]

goroutine 1 [running]:
github.com/AvengeMedia/DankMaterialShell/core/internal/dms.(*Detector).GetDependencyStatus(0x0)
        github.com/AvengeMedia/DankMaterialShell/core/internal/dms/detector.go:56 +0x25
github.com/AvengeMedia/DankMaterialShell/core/internal/dms.(*Detector).GetInstalledComponents(0x421dd1?)
        github.com/AvengeMedia/DankMaterialShell/core/internal/dms/detector.go:120 +0x1f
github.com/AvengeMedia/DankMaterialShell/core/internal/dms.NewModel({_, _})
        github.com/AvengeMedia/DankMaterialShell/core/internal/dms/app.go:108 +0x67
main.runInteractiveMode(0xc0001e3000?, {0xdabb80?, 0x4?, 0xdabae0?})
        github.com/AvengeMedia/DankMaterialShell/core/cmd/dms/commands_root.go:85 +0x85
github.com/spf13/cobra.(*Command).execute(0x1549460, {0xc0000360d0, 0x0, 0x0})
        github.com/spf13/cobra@v1.10.1/command.go:1019 +0xae7
github.com/spf13/cobra.(*Command).ExecuteC(0x1549460)
        github.com/spf13/cobra@v1.10.1/command.go:1148 +0x465
github.com/spf13/cobra.(*Command).Execute(...)
        github.com/spf13/cobra@v1.10.1/command.go:1071
main.main()
        github.com/AvengeMedia/DankMaterialShell/core/cmd/dms/main.go:41 +0x6a
```
2025-12-02 09:26:06 -05:00
Marcus Ramberg
67ff7726e0 make pre-commit more portable (#880) 2025-12-02 09:25:08 -05:00
215 changed files with 39242 additions and 16763 deletions

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
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)
if [[ -n "$STAGED_CORE_FILES" ]]; then
echo "Go files staged in core/, running CI checks..."
cd "$REPO_ROOT/core"
echo "Go files staged in core/, running CI checks..."
cd "$REPO_ROOT/core"
# Format check
echo " Checking gofmt..."
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
if [[ -n "$UNFORMATTED" ]]; then
echo "The following files are not formatted:"
echo "$UNFORMATTED"
echo ""
echo "Run: cd core && gofmt -s -w ."
exit 1
fi
# Format check
echo " Checking gofmt..."
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
if [[ -n "$UNFORMATTED" ]]; then
echo "The following files are not formatted:"
echo "$UNFORMATTED"
echo ""
echo "Run: cd core && gofmt -s -w ."
exit 1
fi
# golangci-lint
if command -v golangci-lint &>/dev/null; then
echo " Running golangci-lint..."
golangci-lint run ./...
else
echo " Warning: golangci-lint not installed, skipping lint"
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
fi
# golangci-lint
if command -v golangci-lint &>/dev/null; then
echo " Running golangci-lint..."
golangci-lint run ./...
else
echo " Warning: golangci-lint not installed, skipping lint"
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
fi
# Tests
echo " Running tests..."
go test ./... > /dev/null
# Tests
echo " Running tests..."
if ! go test ./... >/dev/null 2>&1; then
echo "Tests failed! Run 'go test ./...' for details."
exit 1
fi
# Build checks
echo " Building..."
mkdir -p bin
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/dankinstall ./cmd/dankinstall
# Build checks
echo " Building..."
mkdir -p bin
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/dankinstall ./cmd/dankinstall
echo "All Go CI checks passed!"
cd "$REPO_ROOT"
echo "All Go CI checks passed!"
cd "$REPO_ROOT"
fi
# =============================================================================

View File

@@ -517,7 +517,6 @@ jobs:
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: wl-clipboard
Recommends: NetworkManager
@@ -610,6 +609,8 @@ jobs:
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true

View File

@@ -61,56 +61,44 @@ jobs:
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get latest commit hash from master branch
LATEST_COMMIT=$(git rev-parse origin/master 2>/dev/null || git rev-parse master 2>/dev/null || echo "")
if [[ -z "$LATEST_COMMIT" ]]; then
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Could not determine git commit, proceeding with update"
else
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Check tarball age - if older than 3 hours, update needed
if [[ -f "dms-git-source.tar.gz" ]]; 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
# Get current commit hash (8 chars to match spec format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Check OBS for last uploaded commit
OBS_BASE="$HOME/.cache/osc-checkouts"
mkdir -p "$OBS_BASE"
OBS_PROJECT="home:AvengeMedia:dms-git"
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
osc up -q 2>/dev/null || true
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
if [[ -f "dms-git.spec" ]]; then
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$OBS_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 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
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No existing tarball in OBS, update needed"
echo "📋 Could not extract OBS commit, proceeding with update"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
echo "📋 No spec file in OBS, proceeding with update"
fi
cd "${{ github.workspace }}"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 First upload to OBS, update needed"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
@@ -171,7 +159,52 @@ jobs:
DATE_STR=$(date "+%a %b %d %Y")
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
- name: Update Debian dms-git changelog version
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
run: |
# Get commit info for dms-git versioning
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
# Debian version format: 0.6.2+git2256.9162e314
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
CHANGELOG_DATE=$(date -R)
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
# Get current version from changelog
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
echo "Current Debian version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Only update if version changed
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
# Create new changelog entry at top
TEMP_CHANGELOG=$(mktemp)
cat > "$TEMP_CHANGELOG" << EOF
dms-git ($NEW_VERSION) nightly; urgency=medium
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
EOF
# Prepend to existing changelog
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
else
echo "✓ Debian changelog already at version $NEW_VERSION"
fi
- name: Update dms stable version
if: steps.packages.outputs.version != ''
run: |

View File

@@ -15,9 +15,70 @@ on:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
jobs:
upload-ppa:
check-updates:
name: Check for updates
runs-on: ubuntu-latest
outputs:
has_updates: ${{ steps.check.outputs.has_updates }}
packages: ${{ steps.check.outputs.packages }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check for updates
id: check
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Checking if dms-git source has changed..."
# Get current commit hash (8 chars to match changelog format)
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
# Extract commit hash from changelog
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
if [[ -f "$CHANGELOG_FILE" ]]; then
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
if [[ -n "$CHANGELOG_COMMIT" ]]; then
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
echo "has_updates=false" >> $GITHUB_OUTPUT
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 Could not extract commit from changelog, proceeding with upload"
fi
else
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "📋 No changelog file found, proceeding with upload"
fi
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
fi
upload-ppa:
name: Upload to PPA
needs: check-updates
runs-on: ubuntu-latest
if: |
github.event_name == 'workflow_dispatch' ||
needs.check-updates.outputs.has_updates == 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -54,13 +115,13 @@ jobs:
id: packages
run: |
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"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
fi
- name: Upload to PPA
@@ -102,7 +163,11 @@ jobs:
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
fi
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY

View File

@@ -43,6 +43,7 @@ install-shell:
@mkdir -p $(SHELL_INSTALL_DIR)
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
@echo "Shell files installed"
install-completions:
@@ -80,8 +81,7 @@ install: build install-bin install-shell install-completions install-systemd ins
@echo ""
@echo "Installation complete!"
@echo ""
@echo "To enable and start DMS:"
@echo " systemctl --user enable --now dms"
@echo "=== Cheers, the DMS Team! ==="
# Uninstallation targets
uninstall-bin:

View File

@@ -6,5 +6,5 @@ Exec=dms open %u
Icon=danklogo
Terminal=false
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;

View File

@@ -5,7 +5,8 @@ After=graphical-session.target
Requisite=graphical-session.target
[Service]
Type=simple
Type=dbus
BusName=org.freedesktop.Notifications
ExecStart=/usr/bin/dms run --session
ExecReload=/usr/bin/pkill -USR1 -x dms
Restart=always

View File

@@ -10,16 +10,19 @@ GO=go
GOFLAGS=-ldflags="-s -w"
# Version and build info
VERSION=$(shell git describe --tags --always 2>/dev/null || echo "dev")
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BASE_VERSION=$(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
COMMIT_COUNT=$(shell git rev-list --count HEAD 2>/dev/null || echo "0")
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)'
# Architecture to build for dist target (amd64, arm64, or 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
all: build
@@ -132,6 +135,9 @@ version: check-go
@echo "Build Time: $(BUILD_TIME)"
@echo "Commit: $(COMMIT)"
print-version:
@echo "$(VERSION)"
help:
@echo "Available targets:"
@echo " all - Build the main binary (dms) (default)"

View File

@@ -16,6 +16,9 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
**Wayland Protocols**
- `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
- `ext-workspace-v1` - Workspace protocol support
- `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 plugins [install|browse|search]` - Plugin management
- `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 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
Requires Go 1.24+

View File

@@ -28,7 +28,14 @@ var brightnessSetCmd = &cobra.Command{
Short: "Set brightness for a device",
Long: "Set brightness percentage (0-100) for a specific device",
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{
@@ -36,7 +43,14 @@ var brightnessGetCmd = &cobra.Command{
Short: "Get brightness for a device",
Long: "Get current brightness percentage for a specific device",
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() {
@@ -105,9 +119,7 @@ Global Flags:
brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd)
}
func runBrightnessList(cmd *cobra.Command, args []string) {
includeDDC, _ := cmd.Flags().GetBool("ddc")
func getAllBrightnessDevices(includeDDC bool) []brightness.Device {
allDevices := []brightness.Device{}
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 {
fmt.Println("No brightness devices found")
return
@@ -261,31 +280,20 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
log.Fatalf("Failed to set brightness for device: %s", deviceID)
}
func getBrightnessDevices(includeDDC bool) []string {
allDevices := getAllBrightnessDevices(includeDDC)
var deviceIDs []string
for _, device := range allDevices {
deviceIDs = append(deviceIDs, device.ID)
}
return deviceIDs
}
func runBrightnessGet(cmd *cobra.Command, args []string) {
deviceID := args[0]
includeDDC, _ := cmd.Flags().GetBool("ddc")
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...)
}
}
}
allDevices := getAllBrightnessDevices(includeDDC)
for _, device := range allDevices {
if device.ID == deviceID {

View 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()
}

View File

@@ -2,6 +2,8 @@ package main
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -66,6 +68,10 @@ var ipcCmd = &cobra.Command{
Short: "Send IPC commands to running DMS shell",
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
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) {
runShellIPCCommand(args)
},
@@ -115,6 +121,12 @@ var pluginsInstallCmd = &cobra.Command{
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.",
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) {
if err := installPluginCLI(args[0]); err != nil {
log.Fatalf("Error installing plugin: %v", err)
@@ -127,6 +139,12 @@ var pluginsUninstallCmd = &cobra.Command{
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.",
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 := uninstallPluginCLI(args[0]); err != nil {
log.Fatalf("Error uninstalling plugin: %v", err)
@@ -136,7 +154,55 @@ var pluginsUninstallCmd = &cobra.Command{
func runVersion(cmd *cobra.Command, args []string) {
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 {
@@ -299,6 +365,38 @@ func installPluginCLI(idOrName string) error {
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 {
manager, err := plugins.NewManager()
if err != nil {
@@ -373,5 +471,8 @@ func getCommonCommands() []*cobra.Command {
keybindsCmd,
greeterCmd,
setupCmd,
colorCmd,
screenshotCmd,
notifyActionCmd,
}
}

View File

@@ -27,6 +27,9 @@ func init() {
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
dank16Cmd.Flags().String("background", "", "Custom background color")
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
})
}
func runDank16(cmd *cobra.Command, args []string) {

View File

@@ -16,14 +16,26 @@ var dpmsOnCmd = &cobra.Command{
Use: "on [output]",
Short: "Turn display(s) on",
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{
Use: "off [output]",
Short: "Turn display(s) off",
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{
@@ -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) {
client, err := newDPMSClient()
if err != nil {

View File

@@ -30,14 +30,44 @@ var keybindsShowCmd = &cobra.Command{
Short: "Show keybinds for a provider",
Long: "Display keybinds/cheatsheet for the specified provider",
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() {
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(keybindsShowCmd)
keybindsCmd.AddCommand(keybindsSetCmd)
keybindsCmd.AddCommand(keybindsRemoveCmd)
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
return providers.NewJSONFileProvider(filePath)
@@ -75,69 +105,122 @@ func initializeProviders() {
}
}
func runKeybindsList(cmd *cobra.Command, args []string) {
registry := keybinds.GetDefaultRegistry()
providers := registry.List()
if len(providers) == 0 {
func runKeybindsList(_ *cobra.Command, _ []string) {
providerList := keybinds.GetDefaultRegistry().List()
if len(providerList) == 0 {
fmt.Fprintln(os.Stdout, "No providers available")
return
}
fmt.Fprintln(os.Stdout, "Available providers:")
for _, name := range providers {
for _, name := range providerList {
fmt.Fprintf(os.Stdout, " - %s\n", name)
}
}
func runKeybindsShow(cmd *cobra.Command, args []string) {
providerName := args[0]
registry := keybinds.GetDefaultRegistry()
customPath, _ := cmd.Flags().GetString("path")
if customPath != "" {
var provider keybinds.Provider
switch providerName {
case "hyprland":
provider = providers.NewHyprlandProvider(customPath)
case "mangowc":
provider = providers.NewMangoWCProvider(customPath)
case "sway":
provider = providers.NewSwayProvider(customPath)
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 makeProviderWithPath(name, path string) keybinds.Provider {
switch name {
case "hyprland":
return providers.NewHyprlandProvider(path)
case "mangowc":
return providers.NewMangoWCProvider(path)
case "sway":
return providers.NewSwayProvider(path)
case "niri":
return providers.NewNiriProvider(path)
default:
return nil
}
}
func printCheatSheet(provider keybinds.Provider) {
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))
}
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))
}

View File

@@ -45,6 +45,9 @@ func init() {
openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications")
openCmd.Flags().StringSliceVar(&openCategories, "category", []string{}, "Application categories to filter (e.g., WebBrowser, Office, Graphics)")
openCmd.Flags().StringVar(&openRequestType, "type", "url", "Request type (url, file, or custom)")
_ = openCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"url", "file", "custom"}, cobra.ShellCompDirectiveNoFileComp
})
}
// mimeTypeToCategories maps MIME types to desktop file categories

View File

@@ -0,0 +1,377 @@
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
"github.com/spf13/cobra"
)
var (
ssOutputName string
ssIncludeCursor bool
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
)
var screenshotCmd = &cobra.Command{
Use: "screenshot",
Short: "Capture screenshots",
Long: `Capture screenshots from Wayland displays.
Modes:
region - Select a region interactively (default)
full - Capture the focused output
all - Capture all outputs combined
output - Capture a specific output by name
window - Capture the focused window (Hyprland only)
last - Capture the last selected region
Output format (--format):
png - PNG format (default)
jpg/jpeg - JPEG format
ppm - PPM format
Examples:
dms screenshot # Region select, save file + clipboard
dms screenshot full # Full screen of focused output
dms screenshot all # All screens combined
dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland)
dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only
dms screenshot --cursor # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
}
var ssRegionCmd = &cobra.Command{
Use: "region",
Short: "Select a region interactively",
Run: runScreenshotRegion,
}
var ssFullCmd = &cobra.Command{
Use: "full",
Short: "Capture the focused output",
Run: runScreenshotFull,
}
var ssAllCmd = &cobra.Command{
Use: "all",
Short: "Capture all outputs combined",
Run: runScreenshotAll,
}
var ssOutputCmd = &cobra.Command{
Use: "output",
Short: "Capture a specific output",
Run: runScreenshotOutput,
}
var ssLastCmd = &cobra.Command{
Use: "last",
Short: "Capture the last selected region",
Long: `Capture the previously selected region without interactive selection.
If no previous region exists, falls back to interactive selection.`,
Run: runScreenshotLast,
}
var ssWindowCmd = &cobra.Command{
Use: "window",
Short: "Capture the focused window",
Long: `Capture the currently focused window.
Currently only supported on Hyprland.`,
Run: runScreenshotWindow,
}
var ssListCmd = &cobra.Command{
Use: "list",
Short: "List available outputs",
Run: runScreenshotList,
}
var notifyActionCmd = &cobra.Command{
Use: "notify-action",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
screenshot.RunNotifyActionListener(args)
},
}
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)")
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.AddCommand(ssRegionCmd)
screenshotCmd.AddCommand(ssFullCmd)
screenshotCmd.AddCommand(ssAllCmd)
screenshotCmd.AddCommand(ssOutputCmd)
screenshotCmd.AddCommand(ssLastCmd)
screenshotCmd.AddCommand(ssWindowCmd)
screenshotCmd.AddCommand(ssListCmd)
screenshotCmd.Run = runScreenshotRegion
}
func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.IncludeCursor = ssIncludeCursor
config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify
config.Stdout = ssStdout
if ssOutputDir != "" {
config.OutputDir = ssOutputDir
}
if ssFilename != "" {
config.Filename = ssFilename
}
switch strings.ToLower(ssFormat) {
case "jpg", "jpeg":
config.Format = screenshot.FormatJPEG
case "ppm":
config.Format = screenshot.FormatPPM
default:
config.Format = screenshot.FormatPNG
}
if ssQuality < 1 {
ssQuality = 1
}
if ssQuality > 100 {
ssQuality = 100
}
config.Quality = ssQuality
return config
}
func runScreenshot(config screenshot.Config) {
sc := screenshot.New(config)
result, err := sc.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if result == nil {
os.Exit(0)
}
defer result.Buffer.Close()
if result.YInverted {
result.Buffer.FlipVertical()
}
if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
os.Exit(1)
}
return
}
var filePath string
if config.SaveFile {
outputDir := config.OutputDir
if outputDir == "" {
outputDir = screenshot.GetOutputDir()
}
filename := config.Filename
if filename == "" {
filename = screenshot.GenerateFilename(config.Format)
}
filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
os.Exit(1)
}
fmt.Println(filePath)
}
if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
os.Exit(1)
}
if !config.SaveFile {
fmt.Println("Copied to clipboard")
}
}
if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{
FilePath: filePath,
Clipboard: config.Clipboard,
ImageData: thumbData,
Width: thumbW,
Height: thumbH,
})
}
}
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
var mimeType string
var data bytes.Buffer
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
mimeType = "image/jpeg"
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
return err
}
default:
mimeType = "image/png"
if err := screenshot.EncodePNG(&data, img); err != nil {
return err
}
}
cmd := exec.Command("wl-copy", "--type", mimeType)
cmd.Stdin = &data
return cmd.Run()
}
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
switch format {
case screenshot.FormatJPEG:
return screenshot.EncodeJPEG(os.Stdout, img, quality)
default:
return screenshot.EncodePNG(os.Stdout, img)
}
}
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
srcW, srcH := buf.Width, buf.Height
scale := 1.0
if srcW > maxSize || srcH > maxSize {
if srcW > srcH {
scale = float64(maxSize) / float64(srcW)
} else {
scale = float64(maxSize) / float64(srcH)
}
}
dstW := int(float64(srcW) * scale)
dstH := int(float64(srcH) * scale)
if dstW < 1 {
dstW = 1
}
if dstH < 1 {
dstH = 1
}
data := buf.Data()
rgb := make([]byte, dstW*dstH*3)
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
for y := 0; y < dstH; y++ {
srcY := int(float64(y) / scale)
if srcY >= srcH {
srcY = srcH - 1
}
for x := 0; x < dstW; x++ {
srcX := int(float64(x) / scale)
if srcX >= srcW {
srcX = srcW - 1
}
si := srcY*buf.Stride + srcX*4
di := (y*dstW + x) * 3
if si+2 < len(data) {
if swapRB {
rgb[di+0] = data[si+2]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+0]
} else {
rgb[di+0] = data[si+0]
rgb[di+1] = data[si+1]
rgb[di+2] = data[si+2]
}
}
}
}
return rgb, dstW, dstH
}
func runScreenshotRegion(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeRegion)
runScreenshot(config)
}
func runScreenshotFull(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeFullScreen)
runScreenshot(config)
}
func runScreenshotAll(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeAllScreens)
runScreenshot(config)
}
func runScreenshotOutput(cmd *cobra.Command, args []string) {
if ssOutputName == "" && len(args) > 0 {
ssOutputName = args[0]
}
if ssOutputName == "" {
fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)")
os.Exit(1)
}
config := getScreenshotConfig(screenshot.ModeOutput)
runScreenshot(config)
}
func runScreenshotLast(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeLastRegion)
runScreenshot(config)
}
func runScreenshotWindow(cmd *cobra.Command, args []string) {
config := getScreenshotConfig(screenshot.ModeWindow)
runScreenshot(config)
}
func runScreenshotList(cmd *cobra.Command, args []string) {
outputs, err := screenshot.ListOutputs()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
for _, o := range outputs {
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
}
}

View File

@@ -16,6 +16,8 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
)
type ipcTargets map[string][]string
var isSessionManaged bool
func execDetachedRestart(targetPID int) {
@@ -68,7 +70,7 @@ func getPIDFilePath() string {
func writePIDFile(childPID int) error {
pidFile := getPIDFilePath()
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0644)
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
}
func removePIDFile() {
@@ -144,7 +146,7 @@ func runShellInteractive(session bool) {
socketPath := server.GetSocketPath()
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)
}
defer os.Remove(configStateFile)
@@ -370,7 +372,7 @@ func runShellDaemon(session bool) {
socketPath := server.GetSocketPath()
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)
}
defer os.Remove(configStateFile)
@@ -473,6 +475,51 @@ func runShellDaemon(session bool) {
}
}
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
targets := map[string][]string{}
var currentTarget string
for _, line := range strings.Split(output, "\n") {
if strings.HasPrefix(line, "target ") {
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
}
if strings.HasPrefix(line, " function") && currentTarget != "" {
currentFunc := strings.TrimPrefix(line, " function ")
currentFunc = strings.SplitN(currentFunc, "(", 2)[0]
targets[currentTarget] = append(targets[currentTarget], currentFunc)
}
}
return targets
}
func getShellIPCCompletions(args []string, toComplete string) []string {
cmdArgs := []string{"-p", configPath, "ipc", "show"}
cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets
if output, err := cmd.Output(); err == nil {
log.Debugf("IPC show output: %s", string(output))
targets = parseTargetsFromIPCShowOutput(string(output))
} else {
log.Debugf("Error getting IPC show output for completions: %v", err)
return nil
}
if len(args) > 0 && args[0] == "call" {
args = args[1:]
}
if len(args) == 0 {
targetNames := make([]string, 0)
targetNames = append(targetNames, "call")
for k := range targets {
targetNames = append(targetNames, k)
}
return targetNames
}
return targets[args[0]]
}
func runShellIPCCommand(args []string) {
if len(args) == 0 {
log.Error("IPC command requires arguments")

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

View File

@@ -0,0 +1,730 @@
package colorpicker
import (
"fmt"
"math"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type Config struct {
Format OutputFormat
CustomFormat string
Lowercase bool
Autocopy bool
Notify bool
}
type Output struct {
wlOutput *client.Output
name string
globalName uint32
x, y int32
width int32
height int32
scale int32
fractionalScale float64
}
type LayerSurface struct {
output *Output
state *SurfaceState
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
wlPool *client.ShmPool
wlBuffer *client.Buffer
bufferBusy bool
oldPool *client.ShmPool
oldBuffer *client.Buffer
scopyBuffer *client.Buffer
configured bool
hidden bool
}
type Picker struct {
config Config
display *client.Display
registry *client.Registry
ctx *client.Context
compositor *client.Compositor
shm *client.Shm
seat *client.Seat
pointer *client.Pointer
keyboard *client.Keyboard
layerShell *wlr_layer_shell.ZwlrLayerShellV1
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
viewporter *wp_viewporter.WpViewporter
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
outputs map[uint32]*Output
outputsMu sync.Mutex
surfaces []*LayerSurface
activeSurface *LayerSurface
running bool
pickedColor *Color
err error
}
func New(config Config) *Picker {
return &Picker{
config: config,
outputs: make(map[uint32]*Output),
}
}
func (p *Picker) Run() (*Color, error) {
if err := p.connect(); err != nil {
return nil, fmt.Errorf("wayland connect: %w", err)
}
defer p.cleanup()
if err := p.setupRegistry(); err != nil {
return nil, fmt.Errorf("registry setup: %w", err)
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if p.screencopy == nil {
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
}
if p.layerShell == nil {
return nil, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
}
if p.seat == nil {
return nil, fmt.Errorf("no seat available")
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if err := p.createSurfaces(); err != nil {
return nil, fmt.Errorf("create surfaces: %w", err)
}
if err := p.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
p.running = true
for p.running {
if err := p.ctx.Dispatch(); err != nil {
p.err = err
break
}
p.checkDone()
}
if p.err != nil {
return nil, p.err
}
return p.pickedColor, nil
}
func (p *Picker) checkDone() {
for _, ls := range p.surfaces {
picked, cancelled := ls.state.IsDone()
switch {
case cancelled:
p.running = false
return
case picked:
color, ok := ls.state.PickColor()
if ok {
p.pickedColor = &color
}
p.running = false
return
}
}
}
func (p *Picker) connect() error {
display, err := client.Connect("")
if err != nil {
return err
}
p.display = display
p.ctx = display.Context()
return nil
}
func (p *Picker) roundtrip() error {
return wlhelpers.Roundtrip(p.display, p.ctx)
}
func (p *Picker) setupRegistry() error {
registry, err := p.display.GetRegistry()
if err != nil {
return err
}
p.registry = registry
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
p.handleGlobal(e)
})
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
p.outputsMu.Lock()
delete(p.outputs, e.Name)
p.outputsMu.Unlock()
})
return nil
}
func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.CompositorInterfaceName:
compositor := client.NewCompositor(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
p.compositor = compositor
}
case client.ShmInterfaceName:
shm := client.NewShm(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
p.shm = shm
}
case client.SeatInterfaceName:
seat := client.NewSeat(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
p.seat = seat
p.setupInput()
}
case client.OutputInterfaceName:
output := client.NewOutput(p.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
p.outputsMu.Lock()
p.outputs[e.Name] = &Output{
wlOutput: output,
globalName: e.Name,
scale: 1,
fractionalScale: 1.0,
}
p.outputsMu.Unlock()
p.setupOutputHandlers(e.Name, output)
}
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
p.layerShell = layerShell
}
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
version := e.Version
if version > 3 {
version = 3
}
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
p.screencopy = screencopy
}
case wp_viewporter.WpViewporterInterfaceName:
viewporter := wp_viewporter.NewWpViewporter(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, viewporter); err == nil {
p.viewporter = viewporter
}
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
p.shortcutsInhibitMgr = mgr
}
}
}
func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.x = e.X
o.y = e.Y
}
p.outputsMu.Unlock()
})
output.SetModeHandler(func(e client.OutputModeEvent) {
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
return
}
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.width = e.Width
o.height = e.Height
}
p.outputsMu.Unlock()
})
output.SetScaleHandler(func(e client.OutputScaleEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.scale = e.Factor
o.fractionalScale = float64(e.Factor)
}
p.outputsMu.Unlock()
})
output.SetNameHandler(func(e client.OutputNameEvent) {
p.outputsMu.Lock()
if o, ok := p.outputs[name]; ok {
o.name = e.Name
}
p.outputsMu.Unlock()
})
}
func (p *Picker) createSurfaces() error {
p.outputsMu.Lock()
outputs := make([]*Output, 0, len(p.outputs))
for _, o := range p.outputs {
outputs = append(outputs, o)
}
p.outputsMu.Unlock()
for _, output := range outputs {
ls, err := p.createLayerSurface(output)
if err != nil {
return fmt.Errorf("output %s: %w", output.name, err)
}
p.surfaces = append(p.surfaces, ls)
}
return nil
}
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
surface, err := p.compositor.CreateSurface()
if err != nil {
return nil, fmt.Errorf("create surface: %w", err)
}
layerSurf, err := p.layerShell.GetLayerSurface(
surface,
output.wlOutput,
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
"dms-colorpicker",
)
if err != nil {
return nil, fmt.Errorf("get layer surface: %w", err)
}
ls := &LayerSurface{
output: output,
state: NewSurfaceState(p.config.Format, p.config.Lowercase),
wlSurface: surface,
layerSurf: layerSurf,
hidden: true, // Start hidden, will show overlay when pointer enters
}
if p.viewporter != nil {
vp, err := p.viewporter.GetViewport(surface)
if err == nil {
ls.viewport = vp
}
}
if err := layerSurf.SetAnchor(
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
); err != nil {
log.Warn("failed to set layer anchor", "err", err)
}
if err := layerSurf.SetExclusiveZone(-1); err != nil {
log.Warn("failed to set exclusive zone", "err", err)
}
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
log.Warn("failed to set keyboard interactivity", "err", err)
}
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
if err := layerSurf.AckConfigure(e.Serial); err != nil {
log.Warn("failed to ack configure", "err", err)
}
if err := ls.state.OnLayerConfigure(int(e.Width), int(e.Height)); err != nil {
log.Warn("failed to handle layer configure", "err", err)
}
ls.configured = true
scale := p.computeSurfaceScale(ls)
ls.state.SetScale(scale)
if !ls.state.IsReady() {
p.captureForSurface(ls)
} else {
p.redrawSurface(ls)
}
// Request shortcut inhibition once surface is configured
p.ensureShortcutsInhibitor(ls)
})
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
p.running = false
})
if err := surface.Commit(); err != nil {
log.Warn("failed to commit surface", "err", err)
}
return ls, nil
}
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
out := ls.output
if out == nil || out.fractionalScale <= 0 {
return 1
}
scale := int32(math.Ceil(out.fractionalScale))
if scale <= 0 {
scale = 1
}
return scale
}
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
if p.shortcutsInhibitMgr == nil || p.seat == nil || p.shortcutsInhibitor != nil {
return
}
inhibitor, err := p.shortcutsInhibitMgr.InhibitShortcuts(ls.wlSurface, p.seat)
if err != nil {
log.Debug("failed to create shortcuts inhibitor", "err", err)
return
}
p.shortcutsInhibitor = inhibitor
inhibitor.SetActiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1ActiveEvent) {
log.Debug("shortcuts inhibitor active")
})
inhibitor.SetInactiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1InactiveEvent) {
log.Debug("shortcuts inhibitor deactivated by compositor")
})
}
func (p *Picker) captureForSurface(ls *LayerSurface) {
frame, err := p.screencopy.CaptureOutput(0, ls.output.wlOutput)
if err != nil {
return
}
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
if err := ls.state.OnScreencopyBuffer(PixelFormat(e.Format), int(e.Width), int(e.Height), int(e.Stride)); err != nil {
log.Error("failed to create screencopy buffer", "err", err)
}
})
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
screenBuf := ls.state.ScreenBuffer()
if screenBuf == nil {
return
}
pool, err := p.shm.CreatePool(screenBuf.Fd(), int32(screenBuf.Size()))
if err != nil {
return
}
wlBuffer, err := pool.CreateBuffer(0, int32(screenBuf.Width), int32(screenBuf.Height), int32(screenBuf.Stride), uint32(ls.state.screenFormat))
if err != nil {
pool.Destroy()
return
}
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
ls.scopyBuffer = wlBuffer
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
if err := frame.Copy(wlBuffer); err != nil {
log.Error("failed to copy frame", "err", err)
}
pool.Destroy()
})
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
ls.state.OnScreencopyFlags(e.Flags)
})
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ls.state.OnScreencopyReady()
scale := p.computeSurfaceScale(ls)
ls.state.SetScale(scale)
frame.Destroy()
p.redrawSurface(ls)
})
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
frame.Destroy()
})
}
func (p *Picker) redrawSurface(ls *LayerSurface) {
var renderBuf *ShmBuffer
if ls.hidden {
renderBuf = ls.state.RedrawScreenOnly()
} else {
renderBuf = ls.state.Redraw()
}
if renderBuf == nil {
return
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
ls.oldBuffer = nil
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
ls.oldPool = nil
}
ls.oldPool = ls.wlPool
ls.oldBuffer = ls.wlBuffer
ls.wlPool = nil
ls.wlBuffer = nil
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
ls.wlPool = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffer = wlBuffer
lsRef := ls
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
lsRef.bufferBusy = false
})
ls.bufferBusy = true
logicalW, logicalH := ls.state.LogicalSize()
if logicalW == 0 || logicalH == 0 {
logicalW = int(ls.output.width)
logicalH = int(ls.output.height)
}
scale := ls.state.Scale()
if scale <= 0 {
scale = 1
}
if ls.viewport != nil {
srcW := float64(renderBuf.Width) / float64(scale)
srcH := float64(renderBuf.Height) / float64(scale)
_ = ls.viewport.SetSource(0, 0, srcW, srcH)
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
}
_ = ls.wlSurface.SetBufferScale(scale)
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
_ = ls.wlSurface.Commit()
ls.state.SwapBuffers()
}
func (p *Picker) hideSurface(ls *LayerSurface) {
if ls == nil || ls.wlSurface == nil || ls.hidden {
return
}
ls.hidden = true
// Redraw without the crosshair overlay
p.redrawSurface(ls)
}
func (p *Picker) setupInput() {
if p.seat == nil {
return
}
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
pointer, err := p.seat.GetPointer()
if err == nil {
p.pointer = pointer
p.setupPointerHandlers()
}
}
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
keyboard, err := p.seat.GetKeyboard()
if err == nil {
p.keyboard = keyboard
p.setupKeyboardHandlers()
}
}
})
}
func (p *Picker) setupPointerHandlers() {
p.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
if err := p.pointer.SetCursor(e.Serial, nil, 0, 0); err != nil {
log.Debug("failed to hide cursor", "err", err)
}
if e.Surface == nil {
return
}
p.activeSurface = nil
surfaceID := e.Surface.ID()
for _, ls := range p.surfaces {
if ls.wlSurface.ID() == surfaceID {
p.activeSurface = ls
break
}
}
if p.activeSurface == nil {
return
}
if p.activeSurface.hidden {
p.activeSurface.hidden = false
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
})
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
if e.Surface == nil {
return
}
surfaceID := e.Surface.ID()
for _, ls := range p.surfaces {
if ls.wlSurface.ID() == surfaceID {
p.hideSurface(ls)
break
}
}
})
p.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
if p.activeSurface == nil {
return
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
})
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
if p.activeSurface == nil {
return
}
p.activeSurface.state.OnPointerButton(e.Button, e.State)
})
}
func (p *Picker) setupKeyboardHandlers() {
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
for _, ls := range p.surfaces {
ls.state.OnKey(e.Key, e.State)
}
})
}
func (p *Picker) cleanup() {
for _, ls := range p.surfaces {
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
}
if ls.wlBuffer != nil {
ls.wlBuffer.Destroy()
}
if ls.wlPool != nil {
ls.wlPool.Destroy()
}
if ls.viewport != nil {
ls.viewport.Destroy()
}
if ls.layerSurf != nil {
ls.layerSurf.Destroy()
}
if ls.wlSurface != nil {
ls.wlSurface.Destroy()
}
if ls.state != nil {
ls.state.Destroy()
}
}
if p.shortcutsInhibitor != nil {
if err := p.shortcutsInhibitor.Destroy(); err != nil {
log.Debug("failed to destroy shortcuts inhibitor", "err", err)
}
p.shortcutsInhibitor = nil
}
if p.shortcutsInhibitMgr != nil {
if err := p.shortcutsInhibitMgr.Destroy(); err != nil {
log.Debug("failed to destroy shortcuts inhibit manager", "err", err)
}
p.shortcutsInhibitMgr = nil
}
if p.viewporter != nil {
p.viewporter.Destroy()
}
if p.screencopy != nil {
p.screencopy.Destroy()
}
if p.pointer != nil {
p.pointer.Release()
}
if p.keyboard != nil {
p.keyboard.Release()
}
if p.display != nil {
p.ctx.Close()
}
}

View File

@@ -0,0 +1,40 @@
package colorpicker
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
type ShmBuffer = shm.Buffer
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride)
}
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
}
func GetPixelColorWithFormat(buf *ShmBuffer, x, y int, format PixelFormat) Color {
if x < 0 || x >= buf.Width || y < 0 || y >= buf.Height {
return Color{}
}
data := buf.Data()
offset := y*buf.Stride + x*4
if offset+3 >= len(data) {
return Color{}
}
if format == FormatABGR8888 || format == FormatXBGR8888 {
return Color{
R: data[offset],
G: data[offset+1],
B: data[offset+2],
A: data[offset+3],
}
}
return Color{
B: data[offset],
G: data[offset+1],
R: data[offset+2],
A: data[offset+3],
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -110,7 +110,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return results, nil
}
// deployNiriConfig handles Niri configuration deployment with backup and merging
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{
ConfigType: "Niri",
@@ -123,6 +122,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
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
if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Niri configuration")
@@ -143,14 +148,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
}
// 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
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
}
// Determine terminal command based on choice
var terminalCommand string
switch terminal {
case deps.TerminalGhostty:
@@ -160,13 +163,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
case deps.TerminalAlacritty:
terminalCommand = "alacritty"
default:
terminalCommand = "ghostty" // fallback to ghostty
terminalCommand = "ghostty"
}
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
// If there was an existing config, merge the output sections
if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
if err != nil {
@@ -182,11 +184,38 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
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
cd.log("Successfully deployed Niri configuration")
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) {
var results []DeploymentResult

View File

@@ -479,9 +479,10 @@ general {
func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "binds {")
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
}
func TestHyprlandConfigStructure(t *testing.T) {

View File

@@ -0,0 +1,5 @@
recent-windows {
highlight {
corner-radius 12
}
}

View 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; }
}

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

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

View File

@@ -214,210 +214,27 @@ window-rule {
match app-id="zoom"
open-floating true
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
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" "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; }
}
debug {
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"

View File

@@ -4,3 +4,15 @@ import _ "embed"
//go:embed embedded/niri.kdl
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

View File

@@ -91,7 +91,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectPolkitAgent())
dependencies = append(dependencies, a.detectAccountsService())
// Hyprland-specific tools
@@ -107,7 +106,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
// Base detections (common across distros)
dependencies = append(dependencies, a.detectMatugen())
dependencies = append(dependencies, a.detectDgop())
dependencies = append(dependencies, a.detectHyprpicker())
dependencies = append(dependencies, a.detectClipboardTools()...)
return dependencies, nil
@@ -127,20 +125,6 @@ func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
}
}
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 {
status := deps.StatusMissing
if a.packageInstalled("accountsservice") {
@@ -178,18 +162,13 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", 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},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
}
switch wm {
case deps.WindowManagerHyprland:
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["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = a.getNiriMapping(variants["niri"])

View File

@@ -61,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectPolkitAgent())
dependencies = append(dependencies, d.detectAccountsService())
if wm == deps.WindowManagerNiri {
@@ -70,7 +69,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectHyprpicker())
dependencies = append(dependencies, d.detectClipboardTools()...)
return dependencies, nil
@@ -90,20 +88,6 @@ func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
}
}
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 {
status := deps.StatusMissing
if d.commandExists("xwayland-satellite") {
@@ -139,33 +123,65 @@ func (d *DebianDistribution) packageInstalled(pkg string) bool {
}
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{
// Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", 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},
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
// DMS packages from OBS with variant support
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
if wm == deps.WindowManagerNiri {
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
niriVariant := variants["niri"]
packages["niri"] = d.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant)
}
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 {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
@@ -238,8 +254,23 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
@@ -254,6 +285,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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
@@ -297,8 +344,9 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
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{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
@@ -306,7 +354,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
variantMap[dep.Name] = dep.Variant
}
packageMap := d.GetPackageMapping(wm)
packageMap := d.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
@@ -326,12 +374,116 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
switch pkgInfo.Repository {
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, manualPkgs, variantMap
return systemPkgs, obsPkgs, manualPkgs, variantMap
}
func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
osInfo, err := GetOSInfo()
if err != nil {
return fmt.Errorf("failed to get OS info: %w", err)
}
// Determine Debian version for OBS repository URL
debianVersion := "Debian_13"
if osInfo.VersionID == "testing" {
debianVersion = "Debian_Testing"
}
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion)
// Check if repository already exists
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
checkCmd := exec.CommandContext(ctx, "test", "-f", listFile)
if checkCmd.Run() == nil {
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
continue
}
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
// Create keyrings directory if it doesn't exist
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
if err := mkdirCmd.Run(); err != nil {
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
}
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
// Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Adding OBS repository %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
}
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
d.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
}
}
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Updating package lists...",
NeedsSudo: true,
CommandInfo: "sudo apt-get update",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
}
}
return nil
}
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -494,30 +646,6 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
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 {
if len(packages) == 0 {
return nil
@@ -527,10 +655,6 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
for _, pkg := range packages {
switch pkg {
case "ghostty":
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
default:
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err)

View File

@@ -76,7 +76,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectPolkitAgent())
dependencies = append(dependencies, f.detectAccountsService())
// Hyprland-specific tools
@@ -92,7 +91,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
// Base detections (common across distros)
dependencies = append(dependencies, f.detectMatugen())
dependencies = append(dependencies, f.detectDgop())
dependencies = append(dependencies, f.detectHyprpicker())
dependencies = append(dependencies, f.detectClipboardTools()...)
return dependencies, nil
@@ -112,20 +110,6 @@ func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
}
}
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 {
cmd := exec.Command("rpm", "-q", pkg)
err := cmd.Run()
@@ -145,9 +129,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", 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},
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
// COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
@@ -160,10 +142,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
switch wm {
case deps.WindowManagerHyprland:
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["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = f.getNiriMapping(variants["niri"])
@@ -194,13 +173,6 @@ func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) Pac
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 {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}

View File

@@ -108,7 +108,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, g.detectMatugen())
dependencies = append(dependencies, g.detectDgop())
dependencies = append(dependencies, g.detectHyprpicker())
dependencies = append(dependencies, g.detectClipboardTools()...)
return dependencies, nil
@@ -190,7 +189,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
"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},
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
@@ -207,10 +205,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
switch wm {
case deps.WindowManagerHyprland:
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["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = g.getNiriMapping(variants["niri"])
@@ -236,10 +231,6 @@ func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) Pac
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 {
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
}

View File

@@ -39,6 +39,7 @@ const (
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeManual RepositoryType = "manual" // Manual build from source

View File

@@ -66,7 +66,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectPolkitAgent())
dependencies = append(dependencies, o.detectAccountsService())
// Hyprland-specific tools
@@ -82,7 +81,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
// Base detections (common across distros)
dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectHyprpicker())
dependencies = append(dependencies, o.detectClipboardTools()...)
return dependencies, nil
@@ -102,20 +100,6 @@ func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
}
}
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 {
cmd := exec.Command("rpm", "-q", pkg)
err := cmd.Run()
@@ -135,34 +119,59 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", 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},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
// Manual builds
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
// DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
}
switch wm {
case deps.WindowManagerHyprland:
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["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
// Niri stable has native package support on openSUSE
niriVariant := variants["niri"]
packages["niri"] = o.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant)
}
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 {
status := deps.StatusMissing
if o.commandExists("xwayland-satellite") {
@@ -294,9 +303,23 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
@@ -311,7 +334,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 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
@@ -325,7 +363,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
}
}
// Phase 4: Configuration
// Configuration
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
@@ -334,7 +372,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting post-installation configuration...",
}
// Phase 5: Complete
// Complete
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
@@ -346,8 +384,9 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
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{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant)
@@ -375,12 +414,80 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
switch pkgInfo.Repository {
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name)
}
}
return systemPkgs, manualPkgs, variantMap
return systemPkgs, obsPkgs, manualPkgs, variantMap
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
repoPath, pkg.RepoURL)
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
if checkCmd.Run() == nil {
o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Enabling OBS repo %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
}
cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
o.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
}
}
// Refresh repositories with GPG auto-import
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Refreshing repositories...",
NeedsSudo: true,
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
}
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to refresh repositories: %w", err)
}
}
return nil
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {

View File

@@ -3,9 +3,7 @@ package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"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.detectQuickshell())
dependencies = append(dependencies, u.detectXDGPortal())
dependencies = append(dependencies, u.detectPolkitAgent())
dependencies = append(dependencies, u.detectAccountsService())
// Hyprland-specific tools
@@ -82,7 +79,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
// Base detections (common across distros)
dependencies = append(dependencies, u.detectMatugen())
dependencies = append(dependencies, u.detectDgop())
dependencies = append(dependencies, u.detectHyprpicker())
dependencies = append(dependencies, u.detectClipboardTools()...)
return dependencies, nil
@@ -102,20 +98,6 @@ func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
}
}
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 {
status := deps.StatusMissing
if u.commandExists("xwayland-satellite") {
@@ -151,6 +133,10 @@ func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
}
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{
// Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem},
@@ -158,37 +144,60 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", 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},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
// Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs)
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
// DMS packages from PPAs
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
}
switch wm {
case deps.WindowManagerHyprland:
// Use the cppiber PPA for 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["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
niriVariant := variants["niri"]
packages["niri"] = u.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = u.getXwaylandSatelliteMapping(niriVariant)
}
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 {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
@@ -365,7 +374,7 @@ func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency,
variantMap[dep.Name] = dep.Variant
}
packageMap := u.GetPackageMapping(wm)
packageMap := u.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
@@ -545,10 +554,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
buildDeps["libxcb1-dev"] = true
buildDeps["libpipewire-0.3-dev"] = true
buildDeps["libpam0g-dev"] = true
case "ghostty":
buildDeps["curl"] = true
buildDeps["libgtk-4-dev"] = true
buildDeps["libadwaita-1-dev"] = true
case "matugen":
buildDeps["curl"] = true
case "cliphist":
@@ -562,10 +567,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
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":
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
@@ -629,40 +630,6 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
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 {
if u.commandExists("go") {
return nil
@@ -710,30 +677,6 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
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 {
if len(packages) == 0 {
return nil
@@ -743,10 +686,6 @@ func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages
for _, pkg := range packages {
switch pkg {
case "ghostty":
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
default:
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err)

View File

@@ -105,14 +105,19 @@ type MenuItem struct {
func NewModel(version string) Model {
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
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
if err != nil {
// Fallback to false if detection fails
hyprlandInstalled = false
niriInstalled = false
// Use the proper detection method for both window managers
hyprlandInstalled, niriInstalled, err = detector.GetWindowManagerStatus()
if err != nil {
// Fallback to false if detection fails
hyprlandInstalled = false
niriInstalled = false
}
}
updateToggles := make(map[string]bool)

View File

@@ -109,6 +109,9 @@ func (m Model) renderAboutView() string {
b.WriteString(normalStyle.Render("Components:"))
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 {
status := "✗"
if dep.Status == 1 {

View File

@@ -3,14 +3,20 @@ package providers
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document"
)
type NiriProvider struct {
configDir string
configDir string
dmsBindsIncluded bool
parsed bool
}
func NewNiriProvider(configDir string) *NiriProvider {
@@ -23,8 +29,7 @@ func NewNiriProvider(configDir string) *NiriProvider {
}
func defaultNiriConfigDir() string {
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome != "" {
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
return filepath.Join(configHome, "niri")
}
@@ -40,22 +45,56 @@ func (n *NiriProvider) Name() string {
}
func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := ParseNiriKeys(n.configDir)
result, err := ParseNiriKeys(n.configDir)
if err != nil {
return nil, fmt.Errorf("failed to parse niri config: %w", err)
}
categorizedBinds := make(map[string][]keybinds.Keybind)
n.convertSection(section, "", categorizedBinds)
n.dmsBindsIncluded = result.DMSBindsIncluded
n.parsed = true
return &keybinds.CheatSheet{
Title: "Niri Keybinds",
Provider: n.Name(),
Binds: categorizedBinds,
}, nil
categorizedBinds := make(map[string][]keybinds.Keybind)
n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
sheet := &keybinds.CheatSheet{
Title: "Niri Keybinds",
Provider: n.Name(),
Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded,
}
if result.DMSStatus != nil {
sheet.DMSStatus = &keybinds.DMSBindsStatus{
Exists: result.DMSStatus.Exists,
Included: result.DMSStatus.Included,
IncludePosition: result.DMSStatus.IncludePosition,
TotalIncludes: result.DMSStatus.TotalIncludes,
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
}
}
return sheet, nil
}
func (n *NiriProvider) 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
if section.Name != "" {
currentSubcat = section.Name
@@ -63,12 +102,12 @@ func (n *NiriProvider) convertSection(section *NiriSection, subcategory string,
for _, kb := range section.Keybinds {
category := n.categorizeByAction(kb.Action)
bind := n.convertKeybind(&kb, currentSubcat)
bind := n.convertKeybind(&kb, currentSubcat, conflicts)
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
for _, child := range section.Children {
n.convertSection(&child, currentSubcat, categorizedBinds)
n.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
}
}
@@ -105,27 +144,50 @@ func (n *NiriProvider) categorizeByAction(action string) string {
}
}
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind {
key := n.formatKey(kb)
desc := kb.Description
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind {
rawAction := n.formatRawAction(kb.Action, kb.Args)
keyStr := n.formatKey(kb)
if desc == "" {
desc = rawAction
source := "config"
if strings.Contains(kb.Source, "dms/binds.kdl") {
source = "dms"
}
return keybinds.Keybind{
Key: key,
Description: desc,
bind := keybinds.Keybind{
Key: keyStr,
Description: kb.Description,
Action: rawAction,
Subcategory: subcategory,
Source: source,
}
if source == "dms" && conflicts != nil {
if conflictKb, ok := conflicts[keyStr]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Description,
Action: n.formatRawAction(conflictKb.Action, conflictKb.Args),
Source: "config",
}
}
}
return bind
}
func (n *NiriProvider) formatRawAction(action string, args []string) string {
if len(args) == 0 {
return action
}
if action == "spawn" && len(args) >= 3 && args[1] == "-c" {
switch args[0] {
case "sh", "bash":
cmd := strings.Join(args[2:], " ")
return fmt.Sprintf("spawn %s -c \"%s\"", args[0], strings.ReplaceAll(cmd, "\"", "\\\""))
}
}
return action + " " + strings.Join(args, " ")
}
@@ -135,3 +197,368 @@ func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string {
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (n *NiriProvider) GetOverridePath() string {
return filepath.Join(n.configDir, "dms", "binds.kdl")
}
func (n *NiriProvider) validateAction(action string) error {
action = strings.TrimSpace(action)
switch {
case action == "":
return fmt.Errorf("action cannot be empty")
case action == "spawn" || action == "spawn ":
return fmt.Errorf("spawn command requires arguments")
case strings.HasPrefix(action, "spawn "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn "))
switch rest {
case "":
return fmt.Errorf("spawn command requires arguments")
case "sh -c \"\"", "sh -c ''", "bash -c \"\"", "bash -c ''":
return fmt.Errorf("shell command cannot be empty")
}
}
return nil
}
func (n *NiriProvider) SetBind(key, action, description string, options map[string]any) error {
if err := n.validateAction(action); err != nil {
return err
}
overridePath := n.GetOverridePath()
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
existingBinds, err := n.loadOverrideBinds()
if err != nil {
existingBinds = make(map[string]*overrideBind)
}
existingBinds[key] = &overrideBind{
Key: key,
Action: action,
Description: description,
Options: options,
}
return n.writeOverrideBinds(existingBinds)
}
func (n *NiriProvider) RemoveBind(key string) error {
existingBinds, err := n.loadOverrideBinds()
if err != nil {
return nil
}
delete(existingBinds, key)
return n.writeOverrideBinds(existingBinds)
}
type overrideBind struct {
Key string
Action string
Description string
Options map[string]any
}
func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
overridePath := n.GetOverridePath()
binds := make(map[string]*overrideBind)
data, err := os.ReadFile(overridePath)
if os.IsNotExist(err) {
return binds, nil
}
if err != nil {
return nil, err
}
parser := NewNiriParser(filepath.Dir(overridePath))
parser.currentSource = overridePath
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return nil, err
}
for _, node := range doc.Nodes {
if node.Name.String() != "binds" || node.Children == nil {
continue
}
for _, child := range node.Children {
kb := parser.parseKeybindNode(child, "")
if kb == nil {
continue
}
keyStr := parser.formatBindKey(kb)
binds[keyStr] = &overrideBind{
Key: keyStr,
Action: n.formatRawAction(kb.Action, kb.Args),
Description: kb.Description,
Options: n.extractOptions(child),
}
}
}
return binds, nil
}
func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
if node.Properties == nil {
return make(map[string]any)
}
opts := make(map[string]any)
if val, ok := node.Properties.Get("repeat"); ok {
opts["repeat"] = val.String() == "true"
}
if val, ok := node.Properties.Get("cooldown-ms"); ok {
opts["cooldown-ms"] = val.String()
}
if val, ok := node.Properties.Get("allow-when-locked"); ok {
opts["allow-when-locked"] = val.String() == "true"
}
return opts
}
func (n *NiriProvider) isRecentWindowsAction(action string) bool {
switch action {
case "next-window", "previous-window":
return true
default:
return false
}
}
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
node := document.NewNode()
node.SetName(bind.Key)
if bind.Options != nil {
if v, ok := bind.Options["repeat"]; ok && v == false {
node.AddProperty("repeat", false, "")
}
if v, ok := bind.Options["cooldown-ms"]; ok {
node.AddProperty("cooldown-ms", v, "")
}
if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
node.AddProperty("allow-when-locked", true, "")
}
}
if bind.Description != "" {
node.AddProperty("hotkey-overlay-title", bind.Description, "")
}
actionNode := n.buildActionNode(bind.Action)
node.AddNode(actionNode)
return node
}
func (n *NiriProvider) buildActionNode(action string) *document.Node {
action = strings.TrimSpace(action)
node := document.NewNode()
parts := n.parseActionParts(action)
if len(parts) == 0 {
node.SetName(action)
return node
}
node.SetName(parts[0])
for _, arg := range parts[1:] {
node.AddArgument(arg, "")
}
return node
}
func (n *NiriProvider) parseActionParts(action string) []string {
var parts []string
var current strings.Builder
var inQuote, escaped bool
for _, r := range action {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
overridePath := n.GetOverridePath()
content := n.generateBindsContent(binds)
if err := n.validateBindsContent(content); err != nil {
return err
}
return os.WriteFile(overridePath, []byte(content), 0644)
}
func (n *NiriProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "workspace"):
return 1
case strings.Contains(action, "window") || strings.Contains(action, "column") ||
strings.Contains(action, "focus") || strings.Contains(action, "move") ||
strings.Contains(action, "swap") || strings.Contains(action, "resize"):
return 2
case strings.HasPrefix(action, "focus-monitor") || strings.Contains(action, "monitor"):
return 3
case strings.Contains(action, "screenshot"):
return 4
case action == "quit" || action == "power-off-monitors" || strings.Contains(action, "dpms"):
return 5
case strings.HasPrefix(action, "spawn"):
return 6
default:
return 7
}
}
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
if len(binds) == 0 {
return "binds {}\n"
}
var regularBinds, recentWindowsBinds []*overrideBind
for _, bind := range binds {
switch {
case n.isRecentWindowsAction(bind.Action):
recentWindowsBinds = append(recentWindowsBinds, bind)
default:
regularBinds = append(regularBinds, bind)
}
}
sort.Slice(regularBinds, func(i, j int) bool {
pi, pj := n.getBindSortPriority(regularBinds[i].Action), n.getBindSortPriority(regularBinds[j].Action)
if pi != pj {
return pi < pj
}
return regularBinds[i].Key < regularBinds[j].Key
})
sort.Slice(recentWindowsBinds, func(i, j int) bool {
return recentWindowsBinds[i].Key < recentWindowsBinds[j].Key
})
var sb strings.Builder
sb.WriteString("binds {\n")
for _, bind := range regularBinds {
n.writeBindNode(&sb, bind, " ")
}
sb.WriteString("}\n")
if len(recentWindowsBinds) > 0 {
sb.WriteString("\nrecent-windows {\n")
sb.WriteString(" binds {\n")
for _, bind := range recentWindowsBinds {
n.writeBindNode(&sb, bind, " ")
}
sb.WriteString(" }\n")
sb.WriteString("}\n")
}
return sb.String()
}
func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, indent string) {
node := n.buildBindNode(bind)
sb.WriteString(indent)
sb.WriteString(node.Name.String())
if node.Properties.Exist() {
sb.WriteString(" ")
sb.WriteString(strings.TrimLeft(node.Properties.String(), " "))
}
sb.WriteString(" { ")
if len(node.Children) > 0 {
child := node.Children[0]
actionName := child.Name.String()
sb.WriteString(actionName)
forceQuote := actionName == "spawn"
for _, arg := range child.Arguments {
sb.WriteString(" ")
n.writeArg(sb, arg.ValueString(), forceQuote)
}
}
sb.WriteString("; }\n")
}
func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) {
if !forceQuote && n.isNumericArg(val) {
sb.WriteString(val)
return
}
sb.WriteString("\"")
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
sb.WriteString("\"")
}
func (n *NiriProvider) isNumericArg(val string) bool {
if val == "" {
return false
}
start := 0
if val[0] == '-' || val[0] == '+' {
if len(val) == 1 {
return false
}
start = 1
}
for i := start; i < len(val); i++ {
if val[i] < '0' || val[i] > '9' {
return false
}
}
return true
}
func (n *NiriProvider) validateBindsContent(content string) error {
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
return fmt.Errorf("failed to write temp file: %w", err)
}
tmpFile.Close()
cmd := exec.Command("niri", "validate", "-c", tmpFile.Name())
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("invalid config: %s", strings.TrimSpace(string(output)))
}
return nil
}

View File

@@ -16,6 +16,7 @@ type NiriKeyBinding struct {
Action string
Args []string
Description string
Source string
}
type NiriSection struct {
@@ -25,32 +26,78 @@ type NiriSection struct {
}
type NiriParser struct {
configDir string
processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding
bindOrder []string
configDir string
processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding
bindOrder []string
currentSource string
dmsBindsIncluded bool
dmsBindsExists bool
includeCount int
dmsIncludePos int
bindsBeforeDMS int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
dmsProcessed bool
dmsBindMap map[string]*NiriKeyBinding
conflictingConfigs map[string]*NiriKeyBinding
}
func NewNiriParser(configDir string) *NiriParser {
return &NiriParser{
configDir: configDir,
processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{},
configDir: configDir,
processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{},
currentSource: "",
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
dmsBindMap: make(map[string]*NiriKeyBinding),
conflictingConfigs: make(map[string]*NiriKeyBinding),
}
}
func (p *NiriParser) Parse() (*NiriSection, error) {
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
configPath := filepath.Join(p.configDir, "config.kdl")
section, err := p.parseFile(configPath, "")
if err != nil {
return nil, err
}
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath, section)
}
section.Keybinds = p.finalizeBinds()
return section, nil
}
func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSection) {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return
}
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
baseDir := filepath.Dir(dmsBindsPath)
p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
p.dmsProcessed = true
}
func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
for _, key := range p.bindOrder {
@@ -63,6 +110,20 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
key := p.formatBindKey(kb)
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
if isDMSBind {
p.dmsBindKeys[key] = true
p.dmsBindMap[key] = kb
} else if p.dmsBindKeys[key] {
p.bindsAfterDMS++
p.conflictingConfigs[key] = kb
p.configBindKeys[key] = true
return
} else {
p.configBindKeys[key] = true
}
if _, exists := p.bindMap[key]; !exists {
p.bindOrder = append(p.bindOrder, key)
}
@@ -101,8 +162,11 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
Name: sectionName,
}
prevSource := p.currentSource
p.currentSource = absPath
baseDir := filepath.Dir(absPath)
p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
return section, nil
}
@@ -127,14 +191,23 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
return
}
includePath := node.Arguments[0].String()
includePath = strings.Trim(includePath, "\"")
includePath := strings.Trim(node.Arguments[0].String(), "\"")
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) {
fullPath = includePath
} else {
fullPath = filepath.Join(baseDir, includePath)
}
if isDMSInclude {
p.dmsProcessed = true
}
includedSection, err := p.parseFile(fullPath, "")
@@ -145,6 +218,10 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
section.Children = append(section.Children, includedSection.Children...)
}
func (p *NiriParser) HasDMSBindsIncluded() bool {
return p.dmsBindsIncluded
}
func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) {
if node.Children == nil {
return
@@ -172,7 +249,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()
if keyCombo == "" {
return nil
@@ -182,19 +259,18 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *
var action string
var args []string
if len(node.Children) > 0 {
actionNode := node.Children[0]
action = actionNode.Name.String()
for _, arg := range actionNode.Arguments {
args = append(args, strings.Trim(arg.String(), "\""))
args = append(args, arg.ValueString())
}
}
description := ""
var description string
if node.Properties != nil {
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
description = strings.Trim(val.String(), "\"")
description = val.ValueString()
}
}
@@ -204,26 +280,79 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, subcategory string) *
Action: action,
Args: args,
Description: description,
Source: p.currentSource,
}
}
func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
parts := strings.Split(combo, "+")
if len(parts) == 0 {
switch len(parts) {
case 0:
return nil, combo
}
if len(parts) == 1 {
case 1:
return nil, parts[0]
default:
return parts[:len(parts)-1], parts[len(parts)-1]
}
}
type NiriParseResult struct {
Section *NiriSection
DMSBindsIncluded bool
DMSStatus *DMSBindsStatusInfo
ConflictingConfigs map[string]*NiriKeyBinding
}
type DMSBindsStatusInfo struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *NiriParser) buildDMSStatus() *DMSBindsStatusInfo {
status := &DMSBindsStatusInfo{
Exists: p.dmsBindsExists,
Included: p.dmsBindsIncluded,
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
}
mods := parts[:len(parts)-1]
key := parts[len(parts)-1]
switch {
case !p.dmsBindsExists:
status.Effective = false
status.StatusMessage = "dms/binds.kdl does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.kdl is not included in config.kdl"
case p.bindsAfterDMS > 0:
status.Effective = true
status.OverriddenBy = p.bindsAfterDMS
status.StatusMessage = "Some DMS binds may be overridden by config binds"
default:
status.Effective = true
status.StatusMessage = "DMS binds are active"
}
return mods, key
return status
}
func ParseNiriKeys(configDir string) (*NiriSection, error) {
func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
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
}

View File

@@ -57,20 +57,20 @@ func TestNiriParseBasicBinds(t *testing.T) {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
foundClose := false
foundFullscreen := false
foundTerminal := false
for _, kb := range section.Keybinds {
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "close-window":
foundClose = true
@@ -116,19 +116,19 @@ func TestNiriParseRecentWindows(t *testing.T) {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(result.Section.Keybinds))
}
foundNext := false
foundPrev := false
for _, kb := range section.Keybinds {
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "next-window":
foundNext = true
@@ -172,13 +172,13 @@ include "dms/binds.kdl"
t.Fatalf("Failed to write include config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 2 {
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)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(result.Section.Keybinds))
}
if len(section.Keybinds) > 0 {
kb := section.Keybinds[0]
if len(result.Section.Keybinds) > 0 {
kb := result.Section.Keybinds[0]
if kb.Description != "Override Terminal" {
t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description)
}
@@ -253,13 +253,13 @@ include "config.kdl"
t.Fatalf("Failed to write other config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 2 {
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)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err)
}
if len(section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 1 {
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)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 0 {
t.Errorf("Expected 0 keybinds, got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 0 {
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)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 3 {
t.Fatalf("Expected 3 unique keybinds, got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 3 {
t.Fatalf("Expected 3 unique keybinds, got %d", len(result.Section.Keybinds))
}
var modT *NiriKeyBinding
for i := range section.Keybinds {
kb := &section.Keybinds[i]
for i := range result.Section.Keybinds {
kb := &result.Section.Keybinds[i]
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" {
modT = kb
break
@@ -416,18 +416,18 @@ binds {
t.Fatalf("Failed to write include config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 4 {
t.Errorf("Expected 4 unique keybinds, got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 unique keybinds, got %d", len(result.Section.Keybinds))
}
bindMap := make(map[string]*NiriKeyBinding)
for i := range section.Keybinds {
kb := &section.Keybinds[i]
for i := range result.Section.Keybinds {
kb := &result.Section.Keybinds[i]
key := ""
for _, m := range kb.Mods {
key += m + "+"
@@ -475,16 +475,16 @@ func TestNiriParseMultipleArgs(t *testing.T) {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseNiriKeys(tmpDir)
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(section.Keybinds) != 1 {
t.Fatalf("Expected 1 keybind, got %d", len(section.Keybinds))
if len(result.Section.Keybinds) != 1 {
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 {
t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args)
}
@@ -496,3 +496,119 @@ 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) != 1 {
t.Errorf("move-column-to-workspace should have 1 arg, got %d", len(kb.Args))
}
case "next-window":
if kb.Key != "Tab" {
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
}
}
}
}

View File

@@ -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) {
tmpDir := t.TempDir()
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"]))
}
}
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")
}
}

View File

@@ -1,19 +1,41 @@
package keybinds
type Keybind struct {
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"`
Key string `json:"key"`
Description string `json:"desc"`
Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
}
type DMSBindsStatus struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
IncludePosition int `json:"includePosition"`
TotalIncludes int `json:"totalIncludes"`
BindsAfterDMS int `json:"bindsAfterDms"`
Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"`
}
type CheatSheet struct {
Title string `json:"title"`
Provider string `json:"provider"`
Binds map[string][]Keybind `json:"binds"`
Title string `json:"title"`
Provider string `json:"provider"`
Binds map[string][]Keybind `json:"binds"`
DMSBindsIncluded bool `json:"dmsBindsIncluded"`
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
}
type Provider interface {
Name() string
GetCheatSheet() (*CheatSheet, error)
}
type WritableProvider interface {
Provider
SetBind(key, action, description string, options map[string]any) error
RemoveBind(key string) error
GetOverridePath() string
}

View File

@@ -0,0 +1,284 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/keyboard-shortcuts-inhibit-unstable-v1.xml
//
// keyboard_shortcuts_inhibit_unstable_v1 Protocol Copyright:
//
// Copyright © 2017 Red Hat Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
package keyboard_shortcuts_inhibit
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZwpKeyboardShortcutsInhibitManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwpKeyboardShortcutsInhibitManagerV1InterfaceName = "zwp_keyboard_shortcuts_inhibit_manager_v1"
// ZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
//
// A global interface used for inhibiting the compositor keyboard shortcuts.
type ZwpKeyboardShortcutsInhibitManagerV1 struct {
client.BaseProxy
}
// NewZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
//
// A global interface used for inhibiting the compositor keyboard shortcuts.
func NewZwpKeyboardShortcutsInhibitManagerV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitManagerV1 {
zwpKeyboardShortcutsInhibitManagerV1 := &ZwpKeyboardShortcutsInhibitManagerV1{}
ctx.Register(zwpKeyboardShortcutsInhibitManagerV1)
return zwpKeyboardShortcutsInhibitManagerV1
}
// Destroy : destroy the keyboard shortcuts inhibitor object
//
// Destroy the keyboard shortcuts inhibitor manager.
func (i *ZwpKeyboardShortcutsInhibitManagerV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// InhibitShortcuts : create a new keyboard shortcuts inhibitor object
//
// Create a new keyboard shortcuts inhibitor object associated with
// the given surface for the given seat.
//
// If shortcuts are already inhibited for the specified seat and surface,
// a protocol error "already_inhibited" is raised by the compositor.
//
// surface: the surface that inhibits the keyboard shortcuts behavior
// seat: the wl_seat for which keyboard shortcuts should be disabled
func (i *ZwpKeyboardShortcutsInhibitManagerV1) InhibitShortcuts(surface *client.Surface, seat *client.Seat) (*ZwpKeyboardShortcutsInhibitorV1, error) {
id := NewZwpKeyboardShortcutsInhibitorV1(i.Context())
const opcode = 1
const _reqBufLen = 8 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], surface.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], seat.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
type ZwpKeyboardShortcutsInhibitManagerV1Error uint32
// ZwpKeyboardShortcutsInhibitManagerV1Error :
const (
// ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited : the shortcuts are already inhibited for this surface
ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited ZwpKeyboardShortcutsInhibitManagerV1Error = 0
)
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Name() string {
switch e {
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
return "already_inhibited"
default:
return ""
}
}
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Value() string {
switch e {
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
return "0"
default:
return ""
}
}
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) String() string {
return e.Name() + "=" + e.Value()
}
// ZwpKeyboardShortcutsInhibitorV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwpKeyboardShortcutsInhibitorV1InterfaceName = "zwp_keyboard_shortcuts_inhibitor_v1"
// ZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
//
// A keyboard shortcuts inhibitor instructs the compositor to ignore
// its own keyboard shortcuts when the associated surface has keyboard
// focus. As a result, when the surface has keyboard focus on the given
// seat, it will receive all key events originating from the specified
// seat, even those which would normally be caught by the compositor for
// its own shortcuts.
//
// The Wayland compositor is however under no obligation to disable
// all of its shortcuts, and may keep some special key combo for its own
// use, including but not limited to one allowing the user to forcibly
// restore normal keyboard events routing in the case of an unwilling
// client. The compositor may also use the same key combo to reactivate
// an existing shortcut inhibitor that was previously deactivated on
// user request.
//
// When the compositor restores its own keyboard shortcuts, an
// "inactive" event is emitted to notify the client that the keyboard
// shortcuts inhibitor is not effectively active for the surface and
// seat any more, and the client should not expect to receive all
// keyboard events.
//
// When the keyboard shortcuts inhibitor is inactive, the client has
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
//
// The user can chose to re-enable a previously deactivated keyboard
// shortcuts inhibitor using any mechanism the compositor may offer,
// in which case the compositor will send an "active" event to notify
// the client.
//
// If the surface is destroyed, unmapped, or loses the seat's keyboard
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
// compositor will restore its own keyboard shortcuts but no "inactive"
// event is emitted in this case.
type ZwpKeyboardShortcutsInhibitorV1 struct {
client.BaseProxy
activeHandler ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc
inactiveHandler ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc
}
// NewZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
//
// A keyboard shortcuts inhibitor instructs the compositor to ignore
// its own keyboard shortcuts when the associated surface has keyboard
// focus. As a result, when the surface has keyboard focus on the given
// seat, it will receive all key events originating from the specified
// seat, even those which would normally be caught by the compositor for
// its own shortcuts.
//
// The Wayland compositor is however under no obligation to disable
// all of its shortcuts, and may keep some special key combo for its own
// use, including but not limited to one allowing the user to forcibly
// restore normal keyboard events routing in the case of an unwilling
// client. The compositor may also use the same key combo to reactivate
// an existing shortcut inhibitor that was previously deactivated on
// user request.
//
// When the compositor restores its own keyboard shortcuts, an
// "inactive" event is emitted to notify the client that the keyboard
// shortcuts inhibitor is not effectively active for the surface and
// seat any more, and the client should not expect to receive all
// keyboard events.
//
// When the keyboard shortcuts inhibitor is inactive, the client has
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
//
// The user can chose to re-enable a previously deactivated keyboard
// shortcuts inhibitor using any mechanism the compositor may offer,
// in which case the compositor will send an "active" event to notify
// the client.
//
// If the surface is destroyed, unmapped, or loses the seat's keyboard
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
// compositor will restore its own keyboard shortcuts but no "inactive"
// event is emitted in this case.
func NewZwpKeyboardShortcutsInhibitorV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitorV1 {
zwpKeyboardShortcutsInhibitorV1 := &ZwpKeyboardShortcutsInhibitorV1{}
ctx.Register(zwpKeyboardShortcutsInhibitorV1)
return zwpKeyboardShortcutsInhibitorV1
}
// Destroy : destroy the keyboard shortcuts inhibitor object
//
// Remove the keyboard shortcuts inhibitor from the associated wl_surface.
func (i *ZwpKeyboardShortcutsInhibitorV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// ZwpKeyboardShortcutsInhibitorV1ActiveEvent : shortcuts are inhibited
//
// This event indicates that the shortcut inhibitor is active.
//
// The compositor sends this event every time compositor shortcuts
// are inhibited on behalf of the surface. When active, the client
// may receive input events normally reserved by the compositor
// (see zwp_keyboard_shortcuts_inhibitor_v1).
//
// This occurs typically when the initial request "inhibit_shortcuts"
// first becomes active or when the user instructs the compositor to
// re-enable and existing shortcuts inhibitor using any mechanism
// offered by the compositor.
type ZwpKeyboardShortcutsInhibitorV1ActiveEvent struct{}
type ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1ActiveEvent)
// SetActiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1ActiveEvent
func (i *ZwpKeyboardShortcutsInhibitorV1) SetActiveHandler(f ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc) {
i.activeHandler = f
}
// ZwpKeyboardShortcutsInhibitorV1InactiveEvent : shortcuts are restored
//
// This event indicates that the shortcuts inhibitor is inactive,
// normal shortcuts processing is restored by the compositor.
type ZwpKeyboardShortcutsInhibitorV1InactiveEvent struct{}
type ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1InactiveEvent)
// SetInactiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1InactiveEvent
func (i *ZwpKeyboardShortcutsInhibitorV1) SetInactiveHandler(f ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc) {
i.inactiveHandler = f
}
func (i *ZwpKeyboardShortcutsInhibitorV1) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.activeHandler == nil {
return
}
var e ZwpKeyboardShortcutsInhibitorV1ActiveEvent
i.activeHandler(e)
case 1:
if i.inactiveHandler == nil {
return
}
var e ZwpKeyboardShortcutsInhibitorV1InactiveEvent
i.inactiveHandler(e)
}
}

View File

@@ -0,0 +1,792 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/wlr-layer-shell-unstable-v1.xml
//
// wlr_layer_shell_unstable_v1 Protocol Copyright:
//
// Copyright © 2017 Drew DeVault
//
// Permission to use, copy, modify, distribute, and sell this
// software and its documentation for any purpose is hereby granted
// without fee, provided that the above copyright notice appear in
// all copies and that both that copyright notice and this permission
// notice appear in supporting documentation, and that the name of
// the copyright holders not be used in advertising or publicity
// pertaining to distribution of the software without specific,
// written prior permission. The copyright holders make no
// representations about the suitability of this software for any
// purpose. It is provided "as is" without express or implied
// warranty.
//
// THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
// SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
// SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
// THIS SOFTWARE.
package wlr_layer_shell
import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
xdg_shell "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/stable/xdg-shell"
)
// ZwlrLayerShellV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrLayerShellV1InterfaceName = "zwlr_layer_shell_v1"
// ZwlrLayerShellV1 : create surfaces that are layers of the desktop
//
// Clients can use this interface to assign the surface_layer role to
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
// rendered with a defined z-depth respective to each other. They may also be
// anchored to the edges and corners of a screen and specify input handling
// semantics. This interface should be suitable for the implementation of
// many desktop shell components, and a broad number of other applications
// that interact with the desktop.
type ZwlrLayerShellV1 struct {
client.BaseProxy
}
// NewZwlrLayerShellV1 : create surfaces that are layers of the desktop
//
// Clients can use this interface to assign the surface_layer role to
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
// rendered with a defined z-depth respective to each other. They may also be
// anchored to the edges and corners of a screen and specify input handling
// semantics. This interface should be suitable for the implementation of
// many desktop shell components, and a broad number of other applications
// that interact with the desktop.
func NewZwlrLayerShellV1(ctx *client.Context) *ZwlrLayerShellV1 {
zwlrLayerShellV1 := &ZwlrLayerShellV1{}
ctx.Register(zwlrLayerShellV1)
return zwlrLayerShellV1
}
// GetLayerSurface : create a layer_surface from a surface
//
// Create a layer surface for an existing surface. This assigns the role of
// layer_surface, or raises a protocol error if another role is already
// assigned.
//
// Creating a layer surface from a wl_surface which has a buffer attached
// or committed is a client error, and any attempts by a client to attach
// or manipulate a buffer prior to the first layer_surface.configure call
// must also be treated as errors.
//
// After creating a layer_surface object and setting it up, the client
// must perform an initial commit without any buffer attached.
// The compositor will reply with a layer_surface.configure event.
// The client must acknowledge it and is then allowed to attach a buffer
// to map the surface.
//
// You may pass NULL for output to allow the compositor to decide which
// output to use. Generally this will be the one that the user most
// recently interacted with.
//
// Clients can specify a namespace that defines the purpose of the layer
// surface.
//
// layer: layer to add this surface to
// namespace: namespace for the layer surface
func (i *ZwlrLayerShellV1) GetLayerSurface(surface *client.Surface, output *client.Output, layer uint32, namespace string) (*ZwlrLayerSurfaceV1, error) {
id := NewZwlrLayerSurfaceV1(i.Context())
const opcode = 0
namespaceLen := client.PaddedLen(len(namespace) + 1)
_reqBufLen := 8 + 4 + 4 + 4 + 4 + (4 + namespaceLen)
_reqBuf := make([]byte, _reqBufLen)
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], surface.ID())
l += 4
if output == nil {
client.PutUint32(_reqBuf[l:l+4], 0)
l += 4
} else {
client.PutUint32(_reqBuf[l:l+4], output.ID())
l += 4
}
client.PutUint32(_reqBuf[l:l+4], uint32(layer))
l += 4
client.PutString(_reqBuf[l:l+(4+namespaceLen)], namespace)
l += (4 + namespaceLen)
err := i.Context().WriteMsg(_reqBuf, nil)
return id, err
}
// Destroy : destroy the layer_shell object
//
// This request indicates that the client will not use the layer_shell
// object any more. Objects that have been created through this instance
// are not affected.
func (i *ZwlrLayerShellV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 1
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type ZwlrLayerShellV1Error uint32
// ZwlrLayerShellV1Error :
const (
// ZwlrLayerShellV1ErrorRole : wl_surface has another role
ZwlrLayerShellV1ErrorRole ZwlrLayerShellV1Error = 0
// ZwlrLayerShellV1ErrorInvalidLayer : layer value is invalid
ZwlrLayerShellV1ErrorInvalidLayer ZwlrLayerShellV1Error = 1
// ZwlrLayerShellV1ErrorAlreadyConstructed : wl_surface has a buffer attached or committed
ZwlrLayerShellV1ErrorAlreadyConstructed ZwlrLayerShellV1Error = 2
)
func (e ZwlrLayerShellV1Error) Name() string {
switch e {
case ZwlrLayerShellV1ErrorRole:
return "role"
case ZwlrLayerShellV1ErrorInvalidLayer:
return "invalid_layer"
case ZwlrLayerShellV1ErrorAlreadyConstructed:
return "already_constructed"
default:
return ""
}
}
func (e ZwlrLayerShellV1Error) Value() string {
switch e {
case ZwlrLayerShellV1ErrorRole:
return "0"
case ZwlrLayerShellV1ErrorInvalidLayer:
return "1"
case ZwlrLayerShellV1ErrorAlreadyConstructed:
return "2"
default:
return ""
}
}
func (e ZwlrLayerShellV1Error) String() string {
return e.Name() + "=" + e.Value()
}
type ZwlrLayerShellV1Layer uint32
// ZwlrLayerShellV1Layer : available layers for surfaces
//
// These values indicate which layers a surface can be rendered in. They
// are ordered by z depth, bottom-most first. Traditional shell surfaces
// will typically be rendered between the bottom and top layers.
// Fullscreen shell surfaces are typically rendered at the top layer.
// Multiple surfaces can share a single layer, and ordering within a
// single layer is undefined.
const (
ZwlrLayerShellV1LayerBackground ZwlrLayerShellV1Layer = 0
ZwlrLayerShellV1LayerBottom ZwlrLayerShellV1Layer = 1
ZwlrLayerShellV1LayerTop ZwlrLayerShellV1Layer = 2
ZwlrLayerShellV1LayerOverlay ZwlrLayerShellV1Layer = 3
)
func (e ZwlrLayerShellV1Layer) Name() string {
switch e {
case ZwlrLayerShellV1LayerBackground:
return "background"
case ZwlrLayerShellV1LayerBottom:
return "bottom"
case ZwlrLayerShellV1LayerTop:
return "top"
case ZwlrLayerShellV1LayerOverlay:
return "overlay"
default:
return ""
}
}
func (e ZwlrLayerShellV1Layer) Value() string {
switch e {
case ZwlrLayerShellV1LayerBackground:
return "0"
case ZwlrLayerShellV1LayerBottom:
return "1"
case ZwlrLayerShellV1LayerTop:
return "2"
case ZwlrLayerShellV1LayerOverlay:
return "3"
default:
return ""
}
}
func (e ZwlrLayerShellV1Layer) String() string {
return e.Name() + "=" + e.Value()
}
// ZwlrLayerSurfaceV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrLayerSurfaceV1InterfaceName = "zwlr_layer_surface_v1"
// ZwlrLayerSurfaceV1 : layer metadata interface
//
// An interface that may be implemented by a wl_surface, for surfaces that
// are designed to be rendered as a layer of a stacked desktop-like
// environment.
//
// Layer surface state (layer, size, anchor, exclusive zone,
// margin, interactivity) is double-buffered, and will be applied at the
// time wl_surface.commit of the corresponding wl_surface is called.
//
// Attaching a null buffer to a layer surface unmaps it.
//
// Unmapping a layer_surface means that the surface cannot be shown by the
// compositor until it is explicitly mapped again. The layer_surface
// returns to the state it had right after layer_shell.get_layer_surface.
// The client can re-map the surface by performing a commit without any
// buffer attached, waiting for a configure event and handling it as usual.
type ZwlrLayerSurfaceV1 struct {
client.BaseProxy
configureHandler ZwlrLayerSurfaceV1ConfigureHandlerFunc
closedHandler ZwlrLayerSurfaceV1ClosedHandlerFunc
}
// NewZwlrLayerSurfaceV1 : layer metadata interface
//
// An interface that may be implemented by a wl_surface, for surfaces that
// are designed to be rendered as a layer of a stacked desktop-like
// environment.
//
// Layer surface state (layer, size, anchor, exclusive zone,
// margin, interactivity) is double-buffered, and will be applied at the
// time wl_surface.commit of the corresponding wl_surface is called.
//
// Attaching a null buffer to a layer surface unmaps it.
//
// Unmapping a layer_surface means that the surface cannot be shown by the
// compositor until it is explicitly mapped again. The layer_surface
// returns to the state it had right after layer_shell.get_layer_surface.
// The client can re-map the surface by performing a commit without any
// buffer attached, waiting for a configure event and handling it as usual.
func NewZwlrLayerSurfaceV1(ctx *client.Context) *ZwlrLayerSurfaceV1 {
zwlrLayerSurfaceV1 := &ZwlrLayerSurfaceV1{}
ctx.Register(zwlrLayerSurfaceV1)
return zwlrLayerSurfaceV1
}
// SetSize : sets the size of the surface
//
// Sets the size of the surface in surface-local coordinates. The
// compositor will display the surface centered with respect to its
// anchors.
//
// If you pass 0 for either value, the compositor will assign it and
// inform you of the assignment in the configure event. You must set your
// anchor to opposite edges in the dimensions you omit; not doing so is a
// protocol error. Both values are 0 by default.
//
// Size is double-buffered, see wl_surface.commit.
func (i *ZwlrLayerSurfaceV1) SetSize(width, height uint32) error {
const opcode = 0
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(width))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(height))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetAnchor : configures the anchor point of the surface
//
// Requests that the compositor anchor the surface to the specified edges
// and corners. If two orthogonal edges are specified (e.g. 'top' and
// 'left'), then the anchor point will be the intersection of the edges
// (e.g. the top left corner of the output); otherwise the anchor point
// will be centered on that edge, or in the center if none is specified.
//
// Anchor is double-buffered, see wl_surface.commit.
func (i *ZwlrLayerSurfaceV1) SetAnchor(anchor uint32) error {
const opcode = 1
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(anchor))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetExclusiveZone : configures the exclusive geometry of this surface
//
// Requests that the compositor avoids occluding an area with other
// surfaces. The compositor's use of this information is
// implementation-dependent - do not assume that this region will not
// actually be occluded.
//
// A positive value is only meaningful if the surface is anchored to one
// edge or an edge and both perpendicular edges. If the surface is not
// anchored, anchored to only two perpendicular edges (a corner), anchored
// to only two parallel edges or anchored to all edges, a positive value
// will be treated the same as zero.
//
// A positive zone is the distance from the edge in surface-local
// coordinates to consider exclusive.
//
// Surfaces that do not wish to have an exclusive zone may instead specify
// how they should interact with surfaces that do. If set to zero, the
// surface indicates that it would like to be moved to avoid occluding
// surfaces with a positive exclusive zone. If set to -1, the surface
// indicates that it would not like to be moved to accommodate for other
// surfaces, and the compositor should extend it all the way to the edges
// it is anchored to.
//
// For example, a panel might set its exclusive zone to 10, so that
// maximized shell surfaces are not shown on top of it. A notification
// might set its exclusive zone to 0, so that it is moved to avoid
// occluding the panel, but shell surfaces are shown underneath it. A
// wallpaper or lock screen might set their exclusive zone to -1, so that
// they stretch below or over the panel.
//
// The default value is 0.
//
// Exclusive zone is double-buffered, see wl_surface.commit.
func (i *ZwlrLayerSurfaceV1) SetExclusiveZone(zone int32) error {
const opcode = 2
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(zone))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetMargin : sets a margin from the anchor point
//
// Requests that the surface be placed some distance away from the anchor
// point on the output, in surface-local coordinates. Setting this value
// for edges you are not anchored to has no effect.
//
// The exclusive zone includes the margin.
//
// Margin is double-buffered, see wl_surface.commit.
func (i *ZwlrLayerSurfaceV1) SetMargin(top, right, bottom, left int32) error {
const opcode = 3
const _reqBufLen = 8 + 4 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(top))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(right))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(bottom))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(left))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetKeyboardInteractivity : requests keyboard events
//
// Set how keyboard events are delivered to this surface. By default,
// layer shell surfaces do not receive keyboard events; this request can
// be used to change this.
//
// This setting is inherited by child surfaces set by the get_popup
// request.
//
// Layer surfaces receive pointer, touch, and tablet events normally. If
// you do not want to receive them, set the input region on your surface
// to an empty region.
//
// Keyboard interactivity is double-buffered, see wl_surface.commit.
func (i *ZwlrLayerSurfaceV1) SetKeyboardInteractivity(keyboardInteractivity uint32) error {
const opcode = 4
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(keyboardInteractivity))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// GetPopup : assign this layer_surface as an xdg_popup parent
//
// This assigns an xdg_popup's parent to this layer_surface. This popup
// should have been created via xdg_surface::get_popup with the parent set
// to NULL, and this request must be invoked before committing the popup's
// initial state.
//
// See the documentation of xdg_popup for more details about what an
// xdg_popup is and how it is used.
func (i *ZwlrLayerSurfaceV1) GetPopup(popup *xdg_shell.Popup) error {
const opcode = 5
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], popup.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// AckConfigure : ack a configure event
//
// When a configure event is received, if a client commits the
// surface in response to the configure event, then the client
// must make an ack_configure request sometime before the commit
// request, passing along the serial of the configure event.
//
// If the client receives multiple configure events before it
// can respond to one, it only has to ack the last configure event.
//
// A client is not required to commit immediately after sending
// an ack_configure request - it may even ack_configure several times
// before its next surface commit.
//
// A client may send multiple ack_configure requests before committing, but
// only the last request sent before a commit indicates which configure
// event the client really is responding to.
//
// serial: the serial from the configure event
func (i *ZwlrLayerSurfaceV1) AckConfigure(serial uint32) error {
const opcode = 6
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(serial))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Destroy : destroy the layer_surface
//
// This request destroys the layer surface.
func (i *ZwlrLayerSurfaceV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 7
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetLayer : change the layer of the surface
//
// Change the layer that the surface is rendered on.
//
// Layer is double-buffered, see wl_surface.commit.
//
// layer: layer to move this surface to
func (i *ZwlrLayerSurfaceV1) SetLayer(layer uint32) error {
const opcode = 8
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(layer))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetExclusiveEdge : set the edge the exclusive zone will be applied to
//
// Requests an edge for the exclusive zone to apply. The exclusive
// edge will be automatically deduced from anchor points when possible,
// but when the surface is anchored to a corner, it will be necessary
// to set it explicitly to disambiguate, as it is not possible to deduce
// which one of the two corner edges should be used.
//
// The edge must be one the surface is anchored to, otherwise the
// invalid_exclusive_edge protocol error will be raised.
func (i *ZwlrLayerSurfaceV1) SetExclusiveEdge(edge uint32) error {
const opcode = 9
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(edge))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type ZwlrLayerSurfaceV1KeyboardInteractivity uint32
// ZwlrLayerSurfaceV1KeyboardInteractivity : types of keyboard interaction possible for a layer shell surface
//
// Types of keyboard interaction possible for layer shell surfaces. The
// rationale for this is twofold: (1) some applications are not interested
// in keyboard events and not allowing them to be focused can improve the
// desktop experience; (2) some applications will want to take exclusive
// keyboard focus.
const (
ZwlrLayerSurfaceV1KeyboardInteractivityNone ZwlrLayerSurfaceV1KeyboardInteractivity = 0
ZwlrLayerSurfaceV1KeyboardInteractivityExclusive ZwlrLayerSurfaceV1KeyboardInteractivity = 1
ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand ZwlrLayerSurfaceV1KeyboardInteractivity = 2
)
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Name() string {
switch e {
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
return "none"
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
return "exclusive"
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
return "on_demand"
default:
return ""
}
}
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Value() string {
switch e {
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
return "0"
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
return "1"
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
return "2"
default:
return ""
}
}
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) String() string {
return e.Name() + "=" + e.Value()
}
type ZwlrLayerSurfaceV1Error uint32
// ZwlrLayerSurfaceV1Error :
const (
// ZwlrLayerSurfaceV1ErrorInvalidSurfaceState : provided surface state is invalid
ZwlrLayerSurfaceV1ErrorInvalidSurfaceState ZwlrLayerSurfaceV1Error = 0
// ZwlrLayerSurfaceV1ErrorInvalidSize : size is invalid
ZwlrLayerSurfaceV1ErrorInvalidSize ZwlrLayerSurfaceV1Error = 1
// ZwlrLayerSurfaceV1ErrorInvalidAnchor : anchor bitfield is invalid
ZwlrLayerSurfaceV1ErrorInvalidAnchor ZwlrLayerSurfaceV1Error = 2
// ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity : keyboard interactivity is invalid
ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity ZwlrLayerSurfaceV1Error = 3
// ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge : exclusive edge is invalid given the surface anchors
ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge ZwlrLayerSurfaceV1Error = 4
)
func (e ZwlrLayerSurfaceV1Error) Name() string {
switch e {
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
return "invalid_surface_state"
case ZwlrLayerSurfaceV1ErrorInvalidSize:
return "invalid_size"
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
return "invalid_anchor"
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
return "invalid_keyboard_interactivity"
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
return "invalid_exclusive_edge"
default:
return ""
}
}
func (e ZwlrLayerSurfaceV1Error) Value() string {
switch e {
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
return "0"
case ZwlrLayerSurfaceV1ErrorInvalidSize:
return "1"
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
return "2"
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
return "3"
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
return "4"
default:
return ""
}
}
func (e ZwlrLayerSurfaceV1Error) String() string {
return e.Name() + "=" + e.Value()
}
type ZwlrLayerSurfaceV1Anchor uint32
// ZwlrLayerSurfaceV1Anchor :
const (
// ZwlrLayerSurfaceV1AnchorTop : the top edge of the anchor rectangle
ZwlrLayerSurfaceV1AnchorTop ZwlrLayerSurfaceV1Anchor = 1
// ZwlrLayerSurfaceV1AnchorBottom : the bottom edge of the anchor rectangle
ZwlrLayerSurfaceV1AnchorBottom ZwlrLayerSurfaceV1Anchor = 2
// ZwlrLayerSurfaceV1AnchorLeft : the left edge of the anchor rectangle
ZwlrLayerSurfaceV1AnchorLeft ZwlrLayerSurfaceV1Anchor = 4
// ZwlrLayerSurfaceV1AnchorRight : the right edge of the anchor rectangle
ZwlrLayerSurfaceV1AnchorRight ZwlrLayerSurfaceV1Anchor = 8
)
func (e ZwlrLayerSurfaceV1Anchor) Name() string {
switch e {
case ZwlrLayerSurfaceV1AnchorTop:
return "top"
case ZwlrLayerSurfaceV1AnchorBottom:
return "bottom"
case ZwlrLayerSurfaceV1AnchorLeft:
return "left"
case ZwlrLayerSurfaceV1AnchorRight:
return "right"
default:
return ""
}
}
func (e ZwlrLayerSurfaceV1Anchor) Value() string {
switch e {
case ZwlrLayerSurfaceV1AnchorTop:
return "1"
case ZwlrLayerSurfaceV1AnchorBottom:
return "2"
case ZwlrLayerSurfaceV1AnchorLeft:
return "4"
case ZwlrLayerSurfaceV1AnchorRight:
return "8"
default:
return ""
}
}
func (e ZwlrLayerSurfaceV1Anchor) String() string {
return e.Name() + "=" + e.Value()
}
// ZwlrLayerSurfaceV1ConfigureEvent : suggest a surface change
//
// The configure event asks the client to resize its surface.
//
// Clients should arrange their surface for the new states, and then send
// an ack_configure request with the serial sent in this configure event at
// some point before committing the new surface.
//
// The client is free to dismiss all but the last configure event it
// received.
//
// The width and height arguments specify the size of the window in
// surface-local coordinates.
//
// The size is a hint, in the sense that the client is free to ignore it if
// it doesn't resize, pick a smaller size (to satisfy aspect ratio or
// resize in steps of NxM pixels). If the client picks a smaller size and
// is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
// surface will be centered on this axis.
//
// If the width or height arguments are zero, it means the client should
// decide its own window dimension.
type ZwlrLayerSurfaceV1ConfigureEvent struct {
Serial uint32
Width uint32
Height uint32
}
type ZwlrLayerSurfaceV1ConfigureHandlerFunc func(ZwlrLayerSurfaceV1ConfigureEvent)
// SetConfigureHandler : sets handler for ZwlrLayerSurfaceV1ConfigureEvent
func (i *ZwlrLayerSurfaceV1) SetConfigureHandler(f ZwlrLayerSurfaceV1ConfigureHandlerFunc) {
i.configureHandler = f
}
// ZwlrLayerSurfaceV1ClosedEvent : surface should be closed
//
// The closed event is sent by the compositor when the surface will no
// longer be shown. The output may have been destroyed or the user may
// have asked for it to be removed. Further changes to the surface will be
// ignored. The client should destroy the resource after receiving this
// event, and create a new surface if they so choose.
type ZwlrLayerSurfaceV1ClosedEvent struct{}
type ZwlrLayerSurfaceV1ClosedHandlerFunc func(ZwlrLayerSurfaceV1ClosedEvent)
// SetClosedHandler : sets handler for ZwlrLayerSurfaceV1ClosedEvent
func (i *ZwlrLayerSurfaceV1) SetClosedHandler(f ZwlrLayerSurfaceV1ClosedHandlerFunc) {
i.closedHandler = f
}
func (i *ZwlrLayerSurfaceV1) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.configureHandler == nil {
return
}
var e ZwlrLayerSurfaceV1ConfigureEvent
l := 0
e.Serial = client.Uint32(data[l : l+4])
l += 4
e.Width = client.Uint32(data[l : l+4])
l += 4
e.Height = client.Uint32(data[l : l+4])
l += 4
i.configureHandler(e)
case 1:
if i.closedHandler == nil {
return
}
var e ZwlrLayerSurfaceV1ClosedEvent
i.closedHandler(e)
}
}

View File

@@ -0,0 +1,532 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/wlr-screencopy-unstable-v1.xml
//
// wlr_screencopy_unstable_v1 Protocol Copyright:
//
// Copyright © 2018 Simon Ser
// Copyright © 2019 Andri Yngvason
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
package wlr_screencopy
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZwlrScreencopyManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrScreencopyManagerV1InterfaceName = "zwlr_screencopy_manager_v1"
// ZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
//
// This object is a manager which offers requests to start capturing from a
// source.
type ZwlrScreencopyManagerV1 struct {
client.BaseProxy
}
// NewZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
//
// This object is a manager which offers requests to start capturing from a
// source.
func NewZwlrScreencopyManagerV1(ctx *client.Context) *ZwlrScreencopyManagerV1 {
zwlrScreencopyManagerV1 := &ZwlrScreencopyManagerV1{}
ctx.Register(zwlrScreencopyManagerV1)
return zwlrScreencopyManagerV1
}
// CaptureOutput : capture an output
//
// Capture the next frame of an entire output.
//
// overlayCursor: composite cursor onto the frame
func (i *ZwlrScreencopyManagerV1) CaptureOutput(overlayCursor int32, output *client.Output) (*ZwlrScreencopyFrameV1, error) {
frame := NewZwlrScreencopyFrameV1(i.Context())
const opcode = 0
const _reqBufLen = 8 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], frame.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
l += 4
client.PutUint32(_reqBuf[l:l+4], output.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return frame, err
}
// CaptureOutputRegion : capture an output's region
//
// Capture the next frame of an output's region.
//
// The region is given in output logical coordinates, see
// xdg_output.logical_size. The region will be clipped to the output's
// extents.
//
// overlayCursor: composite cursor onto the frame
func (i *ZwlrScreencopyManagerV1) CaptureOutputRegion(overlayCursor int32, output *client.Output, x, y, width, height int32) (*ZwlrScreencopyFrameV1, error) {
frame := NewZwlrScreencopyFrameV1(i.Context())
const opcode = 1
const _reqBufLen = 8 + 4 + 4 + 4 + 4 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], frame.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
l += 4
client.PutUint32(_reqBuf[l:l+4], output.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(x))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(y))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(width))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(height))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return frame, err
}
// Destroy : destroy the manager
//
// All objects created by the manager will still remain valid, until their
// appropriate destroy request has been called.
func (i *ZwlrScreencopyManagerV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 2
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// ZwlrScreencopyFrameV1InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZwlrScreencopyFrameV1InterfaceName = "zwlr_screencopy_frame_v1"
// ZwlrScreencopyFrameV1 : a frame ready for copy
//
// This object represents a single frame.
//
// When created, a series of buffer events will be sent, each representing a
// supported buffer type. The "buffer_done" event is sent afterwards to
// indicate that all supported buffer types have been enumerated. The client
// will then be able to send a "copy" request. If the capture is successful,
// the compositor will send a "flags" event followed by a "ready" event.
//
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
// the "buffer" event is guaranteed to be sent.
//
// If the capture failed, the "failed" event is sent. This can happen anytime
// before the "ready" event.
//
// Once either a "ready" or a "failed" event is received, the client should
// destroy the frame.
type ZwlrScreencopyFrameV1 struct {
client.BaseProxy
bufferHandler ZwlrScreencopyFrameV1BufferHandlerFunc
flagsHandler ZwlrScreencopyFrameV1FlagsHandlerFunc
readyHandler ZwlrScreencopyFrameV1ReadyHandlerFunc
failedHandler ZwlrScreencopyFrameV1FailedHandlerFunc
damageHandler ZwlrScreencopyFrameV1DamageHandlerFunc
linuxDmabufHandler ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc
bufferDoneHandler ZwlrScreencopyFrameV1BufferDoneHandlerFunc
}
// NewZwlrScreencopyFrameV1 : a frame ready for copy
//
// This object represents a single frame.
//
// When created, a series of buffer events will be sent, each representing a
// supported buffer type. The "buffer_done" event is sent afterwards to
// indicate that all supported buffer types have been enumerated. The client
// will then be able to send a "copy" request. If the capture is successful,
// the compositor will send a "flags" event followed by a "ready" event.
//
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
// the "buffer" event is guaranteed to be sent.
//
// If the capture failed, the "failed" event is sent. This can happen anytime
// before the "ready" event.
//
// Once either a "ready" or a "failed" event is received, the client should
// destroy the frame.
func NewZwlrScreencopyFrameV1(ctx *client.Context) *ZwlrScreencopyFrameV1 {
zwlrScreencopyFrameV1 := &ZwlrScreencopyFrameV1{}
ctx.Register(zwlrScreencopyFrameV1)
return zwlrScreencopyFrameV1
}
// Copy : copy the frame
//
// Copy the frame to the supplied buffer. The buffer must have the
// correct size, see zwlr_screencopy_frame_v1.buffer and
// zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
// supported format.
//
// If the frame is successfully copied, "flags" and "ready" events are
// sent. Otherwise, a "failed" event is sent.
func (i *ZwlrScreencopyFrameV1) Copy(buffer *client.Buffer) error {
const opcode = 0
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], buffer.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Destroy : delete this object, used or not
//
// Destroys the frame. This request can be sent at any time by the client.
func (i *ZwlrScreencopyFrameV1) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 1
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// CopyWithDamage : copy the frame when it's damaged
//
// Same as copy, except it waits until there is damage to copy.
func (i *ZwlrScreencopyFrameV1) CopyWithDamage(buffer *client.Buffer) error {
const opcode = 2
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], buffer.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type ZwlrScreencopyFrameV1Error uint32
// ZwlrScreencopyFrameV1Error :
const (
// ZwlrScreencopyFrameV1ErrorAlreadyUsed : the object has already been used to copy a wl_buffer
ZwlrScreencopyFrameV1ErrorAlreadyUsed ZwlrScreencopyFrameV1Error = 0
// ZwlrScreencopyFrameV1ErrorInvalidBuffer : buffer attributes are invalid
ZwlrScreencopyFrameV1ErrorInvalidBuffer ZwlrScreencopyFrameV1Error = 1
)
func (e ZwlrScreencopyFrameV1Error) Name() string {
switch e {
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
return "already_used"
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
return "invalid_buffer"
default:
return ""
}
}
func (e ZwlrScreencopyFrameV1Error) Value() string {
switch e {
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
return "0"
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
return "1"
default:
return ""
}
}
func (e ZwlrScreencopyFrameV1Error) String() string {
return e.Name() + "=" + e.Value()
}
type ZwlrScreencopyFrameV1Flags uint32
// ZwlrScreencopyFrameV1Flags :
const (
// ZwlrScreencopyFrameV1FlagsYInvert : contents are y-inverted
ZwlrScreencopyFrameV1FlagsYInvert ZwlrScreencopyFrameV1Flags = 1
)
func (e ZwlrScreencopyFrameV1Flags) Name() string {
switch e {
case ZwlrScreencopyFrameV1FlagsYInvert:
return "y_invert"
default:
return ""
}
}
func (e ZwlrScreencopyFrameV1Flags) Value() string {
switch e {
case ZwlrScreencopyFrameV1FlagsYInvert:
return "1"
default:
return ""
}
}
func (e ZwlrScreencopyFrameV1Flags) String() string {
return e.Name() + "=" + e.Value()
}
// ZwlrScreencopyFrameV1BufferEvent : wl_shm buffer information
//
// Provides information about wl_shm buffer parameters that need to be
// used for this frame. This event is sent once after the frame is created
// if wl_shm buffers are supported.
type ZwlrScreencopyFrameV1BufferEvent struct {
Format uint32
Width uint32
Height uint32
Stride uint32
}
type ZwlrScreencopyFrameV1BufferHandlerFunc func(ZwlrScreencopyFrameV1BufferEvent)
// SetBufferHandler : sets handler for ZwlrScreencopyFrameV1BufferEvent
func (i *ZwlrScreencopyFrameV1) SetBufferHandler(f ZwlrScreencopyFrameV1BufferHandlerFunc) {
i.bufferHandler = f
}
// ZwlrScreencopyFrameV1FlagsEvent : frame flags
//
// Provides flags about the frame. This event is sent once before the
// "ready" event.
type ZwlrScreencopyFrameV1FlagsEvent struct {
Flags uint32
}
type ZwlrScreencopyFrameV1FlagsHandlerFunc func(ZwlrScreencopyFrameV1FlagsEvent)
// SetFlagsHandler : sets handler for ZwlrScreencopyFrameV1FlagsEvent
func (i *ZwlrScreencopyFrameV1) SetFlagsHandler(f ZwlrScreencopyFrameV1FlagsHandlerFunc) {
i.flagsHandler = f
}
// ZwlrScreencopyFrameV1ReadyEvent : indicates frame is available for reading
//
// Called as soon as the frame is copied, indicating it is available
// for reading. This event includes the time at which the presentation took place.
//
// The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
// each component being an unsigned 32-bit value. Whole seconds are in
// tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
// and the additional fractional part in tv_nsec as nanoseconds. Hence,
// for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
// may have an arbitrary offset at start.
//
// After receiving this event, the client should destroy the object.
type ZwlrScreencopyFrameV1ReadyEvent struct {
TvSecHi uint32
TvSecLo uint32
TvNsec uint32
}
type ZwlrScreencopyFrameV1ReadyHandlerFunc func(ZwlrScreencopyFrameV1ReadyEvent)
// SetReadyHandler : sets handler for ZwlrScreencopyFrameV1ReadyEvent
func (i *ZwlrScreencopyFrameV1) SetReadyHandler(f ZwlrScreencopyFrameV1ReadyHandlerFunc) {
i.readyHandler = f
}
// ZwlrScreencopyFrameV1FailedEvent : frame copy failed
//
// This event indicates that the attempted frame copy has failed.
//
// After receiving this event, the client should destroy the object.
type ZwlrScreencopyFrameV1FailedEvent struct{}
type ZwlrScreencopyFrameV1FailedHandlerFunc func(ZwlrScreencopyFrameV1FailedEvent)
// SetFailedHandler : sets handler for ZwlrScreencopyFrameV1FailedEvent
func (i *ZwlrScreencopyFrameV1) SetFailedHandler(f ZwlrScreencopyFrameV1FailedHandlerFunc) {
i.failedHandler = f
}
// ZwlrScreencopyFrameV1DamageEvent : carries the coordinates of the damaged region
//
// This event is sent right before the ready event when copy_with_damage is
// requested. It may be generated multiple times for each copy_with_damage
// request.
//
// The arguments describe a box around an area that has changed since the
// last copy request that was derived from the current screencopy manager
// instance.
//
// The union of all regions received between the call to copy_with_damage
// and a ready event is the total damage since the prior ready event.
type ZwlrScreencopyFrameV1DamageEvent struct {
X uint32
Y uint32
Width uint32
Height uint32
}
type ZwlrScreencopyFrameV1DamageHandlerFunc func(ZwlrScreencopyFrameV1DamageEvent)
// SetDamageHandler : sets handler for ZwlrScreencopyFrameV1DamageEvent
func (i *ZwlrScreencopyFrameV1) SetDamageHandler(f ZwlrScreencopyFrameV1DamageHandlerFunc) {
i.damageHandler = f
}
// ZwlrScreencopyFrameV1LinuxDmabufEvent : linux-dmabuf buffer information
//
// Provides information about linux-dmabuf buffer parameters that need to
// be used for this frame. This event is sent once after the frame is
// created if linux-dmabuf buffers are supported.
type ZwlrScreencopyFrameV1LinuxDmabufEvent struct {
Format uint32
Width uint32
Height uint32
}
type ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc func(ZwlrScreencopyFrameV1LinuxDmabufEvent)
// SetLinuxDmabufHandler : sets handler for ZwlrScreencopyFrameV1LinuxDmabufEvent
func (i *ZwlrScreencopyFrameV1) SetLinuxDmabufHandler(f ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc) {
i.linuxDmabufHandler = f
}
// ZwlrScreencopyFrameV1BufferDoneEvent : all buffer types reported
//
// This event is sent once after all buffer events have been sent.
//
// The client should proceed to create a buffer of one of the supported
// types, and send a "copy" request.
type ZwlrScreencopyFrameV1BufferDoneEvent struct{}
type ZwlrScreencopyFrameV1BufferDoneHandlerFunc func(ZwlrScreencopyFrameV1BufferDoneEvent)
// SetBufferDoneHandler : sets handler for ZwlrScreencopyFrameV1BufferDoneEvent
func (i *ZwlrScreencopyFrameV1) SetBufferDoneHandler(f ZwlrScreencopyFrameV1BufferDoneHandlerFunc) {
i.bufferDoneHandler = f
}
func (i *ZwlrScreencopyFrameV1) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.bufferHandler == nil {
return
}
var e ZwlrScreencopyFrameV1BufferEvent
l := 0
e.Format = client.Uint32(data[l : l+4])
l += 4
e.Width = client.Uint32(data[l : l+4])
l += 4
e.Height = client.Uint32(data[l : l+4])
l += 4
e.Stride = client.Uint32(data[l : l+4])
l += 4
i.bufferHandler(e)
case 1:
if i.flagsHandler == nil {
return
}
var e ZwlrScreencopyFrameV1FlagsEvent
l := 0
e.Flags = client.Uint32(data[l : l+4])
l += 4
i.flagsHandler(e)
case 2:
if i.readyHandler == nil {
return
}
var e ZwlrScreencopyFrameV1ReadyEvent
l := 0
e.TvSecHi = client.Uint32(data[l : l+4])
l += 4
e.TvSecLo = client.Uint32(data[l : l+4])
l += 4
e.TvNsec = client.Uint32(data[l : l+4])
l += 4
i.readyHandler(e)
case 3:
if i.failedHandler == nil {
return
}
var e ZwlrScreencopyFrameV1FailedEvent
i.failedHandler(e)
case 4:
if i.damageHandler == nil {
return
}
var e ZwlrScreencopyFrameV1DamageEvent
l := 0
e.X = client.Uint32(data[l : l+4])
l += 4
e.Y = client.Uint32(data[l : l+4])
l += 4
e.Width = client.Uint32(data[l : l+4])
l += 4
e.Height = client.Uint32(data[l : l+4])
l += 4
i.damageHandler(e)
case 5:
if i.linuxDmabufHandler == nil {
return
}
var e ZwlrScreencopyFrameV1LinuxDmabufEvent
l := 0
e.Format = client.Uint32(data[l : l+4])
l += 4
e.Width = client.Uint32(data[l : l+4])
l += 4
e.Height = client.Uint32(data[l : l+4])
l += 4
i.linuxDmabufHandler(e)
case 6:
if i.bufferDoneHandler == nil {
return
}
var e ZwlrScreencopyFrameV1BufferDoneEvent
i.bufferDoneHandler(e)
}
}

View File

@@ -0,0 +1,399 @@
// Generated by go-wayland-scanner
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
// XML file : /tmp/viewporter.xml
//
// viewporter Protocol Copyright:
//
// Copyright © 2013-2016 Collabora, Ltd.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice (including the next
// paragraph) shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
package wp_viewporter
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// WpViewporterInterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const WpViewporterInterfaceName = "wp_viewporter"
// WpViewporter : surface cropping and scaling
//
// The global interface exposing surface cropping and scaling
// capabilities is used to instantiate an interface extension for a
// wl_surface object. This extended interface will then allow
// cropping and scaling the surface contents, effectively
// disconnecting the direct relationship between the buffer and the
// surface size.
type WpViewporter struct {
client.BaseProxy
}
// NewWpViewporter : surface cropping and scaling
//
// The global interface exposing surface cropping and scaling
// capabilities is used to instantiate an interface extension for a
// wl_surface object. This extended interface will then allow
// cropping and scaling the surface contents, effectively
// disconnecting the direct relationship between the buffer and the
// surface size.
func NewWpViewporter(ctx *client.Context) *WpViewporter {
wpViewporter := &WpViewporter{}
ctx.Register(wpViewporter)
return wpViewporter
}
// Destroy : unbind from the cropping and scaling interface
//
// Informs the server that the client will not be using this
// protocol object anymore. This does not affect any other objects,
// wp_viewport objects included.
func (i *WpViewporter) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// GetViewport : extend surface interface for crop and scale
//
// Instantiate an interface extension for the given wl_surface to
// crop and scale its content. If the given wl_surface already has
// a wp_viewport object associated, the viewport_exists
// protocol error is raised.
//
// surface: the surface
func (i *WpViewporter) GetViewport(surface *client.Surface) (*WpViewport, error) {
id := NewWpViewport(i.Context())
const opcode = 1
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], surface.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
type WpViewporterError uint32
// WpViewporterError :
const (
// WpViewporterErrorViewportExists : the surface already has a viewport object associated
WpViewporterErrorViewportExists WpViewporterError = 0
)
func (e WpViewporterError) Name() string {
switch e {
case WpViewporterErrorViewportExists:
return "viewport_exists"
default:
return ""
}
}
func (e WpViewporterError) Value() string {
switch e {
case WpViewporterErrorViewportExists:
return "0"
default:
return ""
}
}
func (e WpViewporterError) String() string {
return e.Name() + "=" + e.Value()
}
// WpViewportInterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const WpViewportInterfaceName = "wp_viewport"
// WpViewport : crop and scale interface to a wl_surface
//
// An additional interface to a wl_surface object, which allows the
// client to specify the cropping and scaling of the surface
// contents.
//
// This interface works with two concepts: the source rectangle (src_x,
// src_y, src_width, src_height), and the destination size (dst_width,
// dst_height). The contents of the source rectangle are scaled to the
// destination size, and content outside the source rectangle is ignored.
// This state is double-buffered, see wl_surface.commit.
//
// The two parts of crop and scale state are independent: the source
// rectangle, and the destination size. Initially both are unset, that
// is, no scaling is applied. The whole of the current wl_buffer is
// used as the source, and the surface size is as defined in
// wl_surface.attach.
//
// If the destination size is set, it causes the surface size to become
// dst_width, dst_height. The source (rectangle) is scaled to exactly
// this size. This overrides whatever the attached wl_buffer size is,
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
// has no content and therefore no size. Otherwise, the size is always
// at least 1x1 in surface local coordinates.
//
// If the source rectangle is set, it defines what area of the wl_buffer is
// taken as the source. If the source rectangle is set and the destination
// size is not set, then src_width and src_height must be integers, and the
// surface size becomes the source rectangle size. This results in cropping
// without scaling. If src_width or src_height are not integers and
// destination size is not set, the bad_size protocol error is raised when
// the surface state is applied.
//
// The coordinate transformations from buffer pixel coordinates up to
// the surface-local coordinates happen in the following order:
// 1. buffer_transform (wl_surface.set_buffer_transform)
// 2. buffer_scale (wl_surface.set_buffer_scale)
// 3. crop and scale (wp_viewport.set*)
// This means, that the source rectangle coordinates of crop and scale
// are given in the coordinates after the buffer transform and scale,
// i.e. in the coordinates that would be the surface-local coordinates
// if the crop and scale was not applied.
//
// If src_x or src_y are negative, the bad_value protocol error is raised.
// Otherwise, if the source rectangle is partially or completely outside of
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
// when the surface state is applied. A NULL wl_buffer does not raise the
// out_of_buffer error.
//
// If the wl_surface associated with the wp_viewport is destroyed,
// all wp_viewport requests except 'destroy' raise the protocol error
// no_surface.
//
// If the wp_viewport object is destroyed, the crop and scale
// state is removed from the wl_surface. The change will be applied
// on the next wl_surface.commit.
type WpViewport struct {
client.BaseProxy
}
// NewWpViewport : crop and scale interface to a wl_surface
//
// An additional interface to a wl_surface object, which allows the
// client to specify the cropping and scaling of the surface
// contents.
//
// This interface works with two concepts: the source rectangle (src_x,
// src_y, src_width, src_height), and the destination size (dst_width,
// dst_height). The contents of the source rectangle are scaled to the
// destination size, and content outside the source rectangle is ignored.
// This state is double-buffered, see wl_surface.commit.
//
// The two parts of crop and scale state are independent: the source
// rectangle, and the destination size. Initially both are unset, that
// is, no scaling is applied. The whole of the current wl_buffer is
// used as the source, and the surface size is as defined in
// wl_surface.attach.
//
// If the destination size is set, it causes the surface size to become
// dst_width, dst_height. The source (rectangle) is scaled to exactly
// this size. This overrides whatever the attached wl_buffer size is,
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
// has no content and therefore no size. Otherwise, the size is always
// at least 1x1 in surface local coordinates.
//
// If the source rectangle is set, it defines what area of the wl_buffer is
// taken as the source. If the source rectangle is set and the destination
// size is not set, then src_width and src_height must be integers, and the
// surface size becomes the source rectangle size. This results in cropping
// without scaling. If src_width or src_height are not integers and
// destination size is not set, the bad_size protocol error is raised when
// the surface state is applied.
//
// The coordinate transformations from buffer pixel coordinates up to
// the surface-local coordinates happen in the following order:
// 1. buffer_transform (wl_surface.set_buffer_transform)
// 2. buffer_scale (wl_surface.set_buffer_scale)
// 3. crop and scale (wp_viewport.set*)
// This means, that the source rectangle coordinates of crop and scale
// are given in the coordinates after the buffer transform and scale,
// i.e. in the coordinates that would be the surface-local coordinates
// if the crop and scale was not applied.
//
// If src_x or src_y are negative, the bad_value protocol error is raised.
// Otherwise, if the source rectangle is partially or completely outside of
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
// when the surface state is applied. A NULL wl_buffer does not raise the
// out_of_buffer error.
//
// If the wl_surface associated with the wp_viewport is destroyed,
// all wp_viewport requests except 'destroy' raise the protocol error
// no_surface.
//
// If the wp_viewport object is destroyed, the crop and scale
// state is removed from the wl_surface. The change will be applied
// on the next wl_surface.commit.
func NewWpViewport(ctx *client.Context) *WpViewport {
wpViewport := &WpViewport{}
ctx.Register(wpViewport)
return wpViewport
}
// Destroy : remove scaling and cropping from the surface
//
// The associated wl_surface's crop and scale state is removed.
// The change is applied on the next wl_surface.commit.
func (i *WpViewport) Destroy() error {
defer i.Context().Unregister(i)
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetSource : set the source rectangle for cropping
//
// Set the source rectangle of the associated wl_surface. See
// wp_viewport for the description, and relation to the wl_buffer
// size.
//
// If all of x, y, width and height are -1.0, the source rectangle is
// unset instead. Any other set of values where width or height are zero
// or negative, or x or y are negative, raise the bad_value protocol
// error.
//
// The crop and scale state is double-buffered, see wl_surface.commit.
//
// x: source rectangle x
// y: source rectangle y
// width: source rectangle width
// height: source rectangle height
func (i *WpViewport) SetSource(x, y, width, height float64) error {
const opcode = 1
const _reqBufLen = 8 + 4 + 4 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutFixed(_reqBuf[l:l+4], x)
l += 4
client.PutFixed(_reqBuf[l:l+4], y)
l += 4
client.PutFixed(_reqBuf[l:l+4], width)
l += 4
client.PutFixed(_reqBuf[l:l+4], height)
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetDestination : set the surface size for scaling
//
// Set the destination size of the associated wl_surface. See
// wp_viewport for the description, and relation to the wl_buffer
// size.
//
// If width is -1 and height is -1, the destination size is unset
// instead. Any other pair of values for width and height that
// contains zero or negative values raises the bad_value protocol
// error.
//
// The crop and scale state is double-buffered, see wl_surface.commit.
//
// width: surface width
// height: surface height
func (i *WpViewport) SetDestination(width, height int32) error {
const opcode = 2
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(width))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(height))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
type WpViewportError uint32
// WpViewportError :
const (
// WpViewportErrorBadValue : negative or zero values in width or height
WpViewportErrorBadValue WpViewportError = 0
// WpViewportErrorBadSize : destination size is not integer
WpViewportErrorBadSize WpViewportError = 1
// WpViewportErrorOutOfBuffer : source rectangle extends outside of the content area
WpViewportErrorOutOfBuffer WpViewportError = 2
// WpViewportErrorNoSurface : the wl_surface was destroyed
WpViewportErrorNoSurface WpViewportError = 3
)
func (e WpViewportError) Name() string {
switch e {
case WpViewportErrorBadValue:
return "bad_value"
case WpViewportErrorBadSize:
return "bad_size"
case WpViewportErrorOutOfBuffer:
return "out_of_buffer"
case WpViewportErrorNoSurface:
return "no_surface"
default:
return ""
}
}
func (e WpViewportError) Value() string {
switch e {
case WpViewportErrorBadValue:
return "0"
case WpViewportErrorBadSize:
return "1"
case WpViewportErrorOutOfBuffer:
return "2"
case WpViewportErrorNoSurface:
return "3"
default:
return ""
}
}
func (e WpViewportError) String() string {
return e.Name() + "=" + e.Value()
}

View File

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

View 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>

View 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>

View File

@@ -0,0 +1,69 @@
package screenshot
import (
"encoding/json"
"fmt"
"os"
"os/exec"
)
type Compositor int
const (
CompositorUnknown Compositor = iota
CompositorHyprland
)
func DetectCompositor() Compositor {
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
return CompositorHyprland
}
return CompositorUnknown
}
type WindowGeometry struct {
X int32
Y int32
Width int32
Height int32
}
func GetActiveWindow() (*WindowGeometry, error) {
compositor := DetectCompositor()
switch compositor {
case CompositorHyprland:
return getHyprlandActiveWindow()
default:
return nil, fmt.Errorf("window capture requires Hyprland (other compositors not yet supported)")
}
}
type hyprlandWindow struct {
At [2]int32 `json:"at"`
Size [2]int32 `json:"size"`
}
func getHyprlandActiveWindow() (*WindowGeometry, error) {
cmd := exec.Command("hyprctl", "-j", "activewindow")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("hyprctl activewindow: %w", err)
}
var win hyprlandWindow
if err := json.Unmarshal(output, &win); err != nil {
return nil, fmt.Errorf("parse activewindow: %w", err)
}
if win.Size[0] <= 0 || win.Size[1] <= 0 {
return nil, fmt.Errorf("no active window")
}
return &WindowGeometry{
X: win.At[0],
Y: win.At[1],
Width: win.Size[0],
Height: win.Size[1],
}, nil
}

View File

@@ -0,0 +1,197 @@
package screenshot
import (
"bufio"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
"time"
)
func BufferToImage(buf *ShmBuffer) *image.RGBA {
return BufferToImageWithFormat(buf, uint32(FormatARGB8888))
}
func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
data := buf.Data()
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0
for y := 0; y < buf.Height; y++ {
srcOff := y * buf.Stride
dstOff := y * img.Stride
for x := 0; x < buf.Width; x++ {
si := srcOff + x*4
di := dstOff + x*4
if si+3 >= len(data) || di+3 >= len(img.Pix) {
continue
}
if swapRB {
img.Pix[di+0] = data[si+2]
img.Pix[di+1] = data[si+1]
img.Pix[di+2] = data[si+0]
} else {
img.Pix[di+0] = data[si+0]
img.Pix[di+1] = data[si+1]
img.Pix[di+2] = data[si+2]
}
img.Pix[di+3] = 255
}
}
return img
}
func EncodePNG(w io.Writer, img image.Image) error {
enc := png.Encoder{CompressionLevel: png.BestSpeed}
return enc.Encode(w, img)
}
func EncodeJPEG(w io.Writer, img image.Image, quality int) error {
return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
}
func EncodePPM(w io.Writer, img *image.RGBA) error {
bw := bufio.NewWriter(w)
bounds := img.Bounds()
if _, err := fmt.Fprintf(bw, "P6\n%d %d\n255\n", bounds.Dx(), bounds.Dy()); err != nil {
return err
}
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
off := (y-bounds.Min.Y)*img.Stride + (x-bounds.Min.X)*4
if err := bw.WriteByte(img.Pix[off+0]); err != nil {
return err
}
if err := bw.WriteByte(img.Pix[off+1]); err != nil {
return err
}
if err := bw.WriteByte(img.Pix[off+2]); err != nil {
return err
}
}
}
return bw.Flush()
}
func GenerateFilename(format Format) string {
t := time.Now()
ext := "png"
switch format {
case FormatJPEG:
ext = "jpg"
case FormatPPM:
ext = "ppm"
}
return fmt.Sprintf("screenshot_%s.%s", t.Format("2006-01-02_15-04-05"), ext)
}
func GetOutputDir() string {
if dir := os.Getenv("DMS_SCREENSHOT_DIR"); dir != "" {
return dir
}
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
screenshotDir := filepath.Join(xdgPics, "Screenshots")
if err := os.MkdirAll(screenshotDir, 0755); err == nil {
return screenshotDir
}
return xdgPics
}
if home := os.Getenv("HOME"); home != "" {
return home
}
return "."
}
func getXDGPicturesDir() string {
configDir := os.Getenv("XDG_CONFIG_HOME")
if configDir == "" {
home := os.Getenv("HOME")
if home == "" {
return ""
}
configDir = filepath.Join(home, ".config")
}
userDirsFile := filepath.Join(configDir, "user-dirs.dirs")
data, err := os.ReadFile(userDirsFile)
if err != nil {
return ""
}
for _, line := range splitLines(string(data)) {
if len(line) == 0 || line[0] == '#' {
continue
}
const prefix = "XDG_PICTURES_DIR="
if len(line) > len(prefix) && line[:len(prefix)] == prefix {
path := line[len(prefix):]
path = trimQuotes(path)
path = expandHome(path)
return path
}
}
return ""
}
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
lines = append(lines, s[start:i])
start = i + 1
}
}
if start < len(s) {
lines = append(lines, s[start:])
}
return lines
}
func trimQuotes(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
func expandHome(path string) string {
if len(path) >= 5 && path[:5] == "$HOME" {
home := os.Getenv("HOME")
return home + path[5:]
}
if len(path) >= 1 && path[0] == '~' {
home := os.Getenv("HOME")
return home + path[1:]
}
return path
}
func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error {
return WriteToFileWithFormat(buf, path, format, quality, uint32(FormatARGB8888))
}
func WriteToFileWithFormat(buf *ShmBuffer, path string, format Format, quality int, pixelFormat uint32) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
img := BufferToImageWithFormat(buf, pixelFormat)
switch format {
case FormatJPEG:
return EncodeJPEG(f, img, quality)
case FormatPPM:
return EncodePPM(f, img)
default:
return EncodePNG(f, img)
}
}

View 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(&notificationID); 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()
}

View File

@@ -0,0 +1,805 @@
package screenshot
import (
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type SelectionState struct {
hasSelection bool // There's a selection to display (pre-loaded or user-drawn)
dragging bool // User is actively drawing a new selection
surface *OutputSurface // Surface where selection was made
// Surface-local logical coordinates (from pointer events)
anchorX float64
anchorY float64
currentX float64
currentY float64
}
type RenderSlot struct {
shm *ShmBuffer
pool *client.ShmPool
wlBuf *client.Buffer
busy bool
}
type OutputSurface struct {
output *WaylandOutput
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
screenBuf *ShmBuffer
screenBufNoCursor *ShmBuffer
screenFormat uint32
logicalW int
logicalH int
configured bool
yInverted bool
// Triple-buffered render slots
slots [3]*RenderSlot
slotsReady bool
}
type PreCapture struct {
screenBuf *ShmBuffer
screenBufNoCursor *ShmBuffer
format uint32
yInverted bool
}
type RegionSelector struct {
screenshoter *Screenshoter
display *client.Display
registry *client.Registry
ctx *client.Context
compositor *client.Compositor
shm *client.Shm
seat *client.Seat
pointer *client.Pointer
keyboard *client.Keyboard
layerShell *wlr_layer_shell.ZwlrLayerShellV1
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
viewporter *wp_viewporter.WpViewporter
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
outputs map[uint32]*WaylandOutput
outputsMu sync.Mutex
preCapture map[*WaylandOutput]*PreCapture
surfaces []*OutputSurface
activeSurface *OutputSurface
// Cursor surface for crosshair
cursorSurface *client.Surface
cursorBuffer *ShmBuffer
cursorWlBuf *client.Buffer
cursorPool *client.ShmPool
selection SelectionState
pointerX float64
pointerY float64
preSelect Region
showCapturedCursor bool
shiftHeld bool
running bool
cancelled bool
result Region
capturedBuffer *ShmBuffer
capturedRegion Region
}
func NewRegionSelector(s *Screenshoter) *RegionSelector {
return &RegionSelector{
screenshoter: s,
outputs: make(map[uint32]*WaylandOutput),
preCapture: make(map[*WaylandOutput]*PreCapture),
showCapturedCursor: true,
}
}
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
r.preSelect = GetLastRegion()
if err := r.connect(); err != nil {
return nil, false, fmt.Errorf("wayland connect: %w", err)
}
defer r.cleanup()
if err := r.setupRegistry(); err != nil {
return nil, false, fmt.Errorf("registry setup: %w", err)
}
if err := r.roundtrip(); err != nil {
return nil, false, fmt.Errorf("roundtrip after registry: %w", err)
}
switch {
case r.screencopy == nil:
return nil, false, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
case r.layerShell == nil:
return nil, false, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
case r.seat == nil:
return nil, false, fmt.Errorf("no seat available")
case r.compositor == nil:
return nil, false, fmt.Errorf("compositor not available")
case r.shm == nil:
return nil, false, fmt.Errorf("wl_shm not available")
case len(r.outputs) == 0:
return nil, false, fmt.Errorf("no outputs available")
}
if err := r.roundtrip(); err != nil {
return nil, false, fmt.Errorf("roundtrip after protocol check: %w", err)
}
if err := r.preCaptureAllOutputs(); err != nil {
return nil, false, fmt.Errorf("pre-capture: %w", err)
}
if err := r.createSurfaces(); err != nil {
return nil, false, fmt.Errorf("create surfaces: %w", err)
}
_ = r.createCursor()
if err := r.roundtrip(); err != nil {
return nil, false, fmt.Errorf("roundtrip after surfaces: %w", err)
}
r.running = true
for r.running {
if err := r.ctx.Dispatch(); err != nil {
return nil, false, fmt.Errorf("dispatch: %w", err)
}
}
if r.cancelled || r.capturedBuffer == nil {
return nil, r.cancelled, nil
}
yInverted := false
var format uint32
if r.selection.surface != nil {
yInverted = r.selection.surface.yInverted
format = r.selection.surface.screenFormat
}
return &CaptureResult{
Buffer: r.capturedBuffer,
Region: r.result,
YInverted: yInverted,
Format: format,
}, false, nil
}
func (r *RegionSelector) connect() error {
display, err := client.Connect("")
if err != nil {
return err
}
r.display = display
r.ctx = display.Context()
return nil
}
func (r *RegionSelector) roundtrip() error {
return wlhelpers.Roundtrip(r.display, r.ctx)
}
func (r *RegionSelector) setupRegistry() error {
registry, err := r.display.GetRegistry()
if err != nil {
return err
}
r.registry = registry
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
r.handleGlobal(e)
})
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
r.outputsMu.Lock()
delete(r.outputs, e.Name)
r.outputsMu.Unlock()
})
return nil
}
func (r *RegionSelector) handleGlobal(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.CompositorInterfaceName:
comp := client.NewCompositor(r.ctx)
if err := r.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
r.compositor = comp
}
case client.ShmInterfaceName:
shm := client.NewShm(r.ctx)
if err := r.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
r.shm = shm
}
case client.SeatInterfaceName:
seat := client.NewSeat(r.ctx)
if err := r.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
r.seat = seat
r.setupInput()
}
case client.OutputInterfaceName:
output := client.NewOutput(r.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := r.registry.Bind(e.Name, e.Interface, version, output); err == nil {
r.outputsMu.Lock()
r.outputs[e.Name] = &WaylandOutput{
wlOutput: output,
globalName: e.Name,
scale: 1,
}
r.outputsMu.Unlock()
r.setupOutputHandlers(e.Name, output)
}
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
ls := wlr_layer_shell.NewZwlrLayerShellV1(r.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := r.registry.Bind(e.Name, e.Interface, version, ls); err == nil {
r.layerShell = ls
}
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
sc := wlr_screencopy.NewZwlrScreencopyManagerV1(r.ctx)
version := e.Version
if version > 3 {
version = 3
}
if err := r.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
r.screencopy = sc
}
case wp_viewporter.WpViewporterInterfaceName:
vp := wp_viewporter.NewWpViewporter(r.ctx)
if err := r.registry.Bind(e.Name, e.Interface, e.Version, vp); err == nil {
r.viewporter = vp
}
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(r.ctx)
if err := r.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
r.shortcutsInhibitMgr = mgr
}
}
}
func (r *RegionSelector) setupOutputHandlers(name uint32, output *client.Output) {
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
r.outputsMu.Lock()
if o, ok := r.outputs[name]; ok {
o.x = e.X
o.y = e.Y
o.transform = int32(e.Transform)
}
r.outputsMu.Unlock()
})
output.SetModeHandler(func(e client.OutputModeEvent) {
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
return
}
r.outputsMu.Lock()
if o, ok := r.outputs[name]; ok {
o.width = e.Width
o.height = e.Height
}
r.outputsMu.Unlock()
})
output.SetScaleHandler(func(e client.OutputScaleEvent) {
r.outputsMu.Lock()
if o, ok := r.outputs[name]; ok {
o.scale = e.Factor
}
r.outputsMu.Unlock()
})
output.SetNameHandler(func(e client.OutputNameEvent) {
r.outputsMu.Lock()
if o, ok := r.outputs[name]; ok {
o.name = e.Name
}
r.outputsMu.Unlock()
})
}
func (r *RegionSelector) preCaptureAllOutputs() error {
r.outputsMu.Lock()
outputs := make([]*WaylandOutput, 0, len(r.outputs))
for _, o := range r.outputs {
outputs = append(outputs, o)
}
r.outputsMu.Unlock()
pending := len(outputs) * 2
done := make(chan struct{}, pending)
for _, output := range outputs {
pc := &PreCapture{}
r.preCapture[output] = pc
r.preCaptureOutput(output, pc, true, func() { done <- struct{}{} })
r.preCaptureOutput(output, pc, false, func() { done <- struct{}{} })
}
for i := 0; i < pending; i++ {
if err := r.ctx.Dispatch(); err != nil {
return err
}
select {
case <-done:
default:
i--
}
}
return nil
}
func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture, withCursor bool, onReady func()) {
cursor := int32(0)
if withCursor {
cursor = 1
}
frame, err := r.screencopy.CaptureOutput(cursor, output.wlOutput)
if err != nil {
log.Error("screencopy capture failed", "err", err)
onReady()
return
}
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil {
log.Error("create screen buffer failed", "err", err)
return
}
if withCursor {
pc.screenBuf = buf
pc.format = e.Format
} else {
pc.screenBufNoCursor = buf
}
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil {
log.Error("create shm pool failed", "err", err)
return
}
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), e.Format)
if err != nil {
log.Error("create wl_buffer failed", "err", err)
pool.Destroy()
return
}
if err := frame.Copy(wlBuf); err != nil {
log.Error("frame copy failed", "err", err)
}
pool.Destroy()
})
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
if withCursor {
pc.yInverted = (e.Flags & 1) != 0
}
})
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
frame.Destroy()
onReady()
})
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
log.Error("screencopy failed")
frame.Destroy()
onReady()
})
}
func (r *RegionSelector) createSurfaces() error {
r.outputsMu.Lock()
outputs := make([]*WaylandOutput, 0, len(r.outputs))
for _, o := range r.outputs {
outputs = append(outputs, o)
}
r.outputsMu.Unlock()
for _, output := range outputs {
os, err := r.createOutputSurface(output)
if err != nil {
return fmt.Errorf("output %s: %w", output.name, err)
}
r.surfaces = append(r.surfaces, os)
}
return nil
}
func (r *RegionSelector) createCursor() error {
const size = 24
const hotspot = size / 2
surface, err := r.compositor.CreateSurface()
if err != nil {
return fmt.Errorf("create cursor surface: %w", err)
}
r.cursorSurface = surface
buf, err := CreateShmBuffer(size, size, size*4)
if err != nil {
return fmt.Errorf("create cursor buffer: %w", err)
}
r.cursorBuffer = buf
// Draw crosshair
data := buf.Data()
for y := 0; y < size; y++ {
for x := 0; x < size; x++ {
off := (y*size + x) * 4
// Vertical line
if x >= hotspot-1 && x <= hotspot && y >= 2 && y < size-2 {
data[off+0] = 255 // B
data[off+1] = 255 // G
data[off+2] = 255 // R
data[off+3] = 255 // A
continue
}
// Horizontal line
if y >= hotspot-1 && y <= hotspot && x >= 2 && x < size-2 {
data[off+0] = 255
data[off+1] = 255
data[off+2] = 255
data[off+3] = 255
continue
}
// Transparent
data[off+0] = 0
data[off+1] = 0
data[off+2] = 0
data[off+3] = 0
}
}
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil {
return fmt.Errorf("create cursor pool: %w", err)
}
r.cursorPool = pool
wlBuf, err := pool.CreateBuffer(0, size, size, size*4, uint32(FormatARGB8888))
if err != nil {
return fmt.Errorf("create cursor wl_buffer: %w", err)
}
r.cursorWlBuf = wlBuf
if err := surface.Attach(wlBuf, 0, 0); err != nil {
return fmt.Errorf("attach cursor: %w", err)
}
if err := surface.Damage(0, 0, size, size); err != nil {
return fmt.Errorf("damage cursor: %w", err)
}
if err := surface.Commit(); err != nil {
return fmt.Errorf("commit cursor: %w", err)
}
return nil
}
func (r *RegionSelector) createOutputSurface(output *WaylandOutput) (*OutputSurface, error) {
surface, err := r.compositor.CreateSurface()
if err != nil {
return nil, fmt.Errorf("create surface: %w", err)
}
layerSurf, err := r.layerShell.GetLayerSurface(
surface,
output.wlOutput,
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
"dms-screenshot",
)
if err != nil {
return nil, fmt.Errorf("get layer surface: %w", err)
}
os := &OutputSurface{
output: output,
wlSurface: surface,
layerSurf: layerSurf,
}
if r.viewporter != nil {
vp, err := r.viewporter.GetViewport(surface)
if err == nil {
os.viewport = vp
}
}
if err := layerSurf.SetAnchor(
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
); err != nil {
return nil, fmt.Errorf("set anchor: %w", err)
}
if err := layerSurf.SetExclusiveZone(-1); err != nil {
return nil, fmt.Errorf("set exclusive zone: %w", err)
}
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
return nil, fmt.Errorf("set keyboard interactivity: %w", err)
}
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
if err := layerSurf.AckConfigure(e.Serial); err != nil {
log.Error("ack configure failed", "err", err)
return
}
os.logicalW = int(e.Width)
os.logicalH = int(e.Height)
os.configured = true
r.captureForSurface(os)
r.ensureShortcutsInhibitor(os)
})
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
r.running = false
r.cancelled = true
})
if err := surface.Commit(); err != nil {
return nil, fmt.Errorf("surface commit: %w", err)
}
return os, nil
}
func (r *RegionSelector) ensureShortcutsInhibitor(os *OutputSurface) {
if r.shortcutsInhibitMgr == nil || r.seat == nil || r.shortcutsInhibitor != nil {
return
}
inhibitor, err := r.shortcutsInhibitMgr.InhibitShortcuts(os.wlSurface, r.seat)
if err == nil {
r.shortcutsInhibitor = inhibitor
}
}
func (r *RegionSelector) captureForSurface(os *OutputSurface) {
pc := r.preCapture[os.output]
if pc == nil {
return
}
os.screenBuf = pc.screenBuf
os.screenBufNoCursor = pc.screenBufNoCursor
os.screenFormat = pc.format
os.yInverted = pc.yInverted
r.initRenderBuffer(os)
r.applyPreSelection(os)
r.redrawSurface(os)
}
func (r *RegionSelector) initRenderBuffer(os *OutputSurface) {
if os.screenBuf == nil {
return
}
for i := 0; i < 3; i++ {
slot := &RenderSlot{}
buf, err := CreateShmBuffer(os.screenBuf.Width, os.screenBuf.Height, os.screenBuf.Stride)
if err != nil {
log.Error("create render slot buffer failed", "err", err)
return
}
slot.shm = buf
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil {
log.Error("create render slot pool failed", "err", err)
buf.Close()
return
}
slot.pool = pool
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), os.screenFormat)
if err != nil {
log.Error("create render slot wl_buffer failed", "err", err)
pool.Destroy()
buf.Close()
return
}
slot.wlBuf = wlBuf
slotRef := slot
wlBuf.SetReleaseHandler(func(e client.BufferReleaseEvent) {
slotRef.busy = false
})
os.slots[i] = slot
}
os.slotsReady = true
}
func (os *OutputSurface) acquireFreeSlot() *RenderSlot {
for _, slot := range os.slots {
if slot != nil && !slot.busy {
return slot
}
}
return nil
}
func (r *RegionSelector) applyPreSelection(os *OutputSurface) {
if r.preSelect.IsEmpty() || os.screenBuf == nil || r.selection.hasSelection {
return
}
if r.preSelect.Output != "" && r.preSelect.Output != os.output.name {
return
}
scaleX := float64(os.logicalW) / float64(os.screenBuf.Width)
scaleY := float64(os.logicalH) / float64(os.screenBuf.Height)
x1 := float64(r.preSelect.X-os.output.x) * scaleX
y1 := float64(r.preSelect.Y-os.output.y) * scaleY
x2 := float64(r.preSelect.X-os.output.x+r.preSelect.Width) * scaleX
y2 := float64(r.preSelect.Y-os.output.y+r.preSelect.Height) * scaleY
r.selection.hasSelection = true
r.selection.dragging = false
r.selection.surface = os
r.selection.anchorX = x1
r.selection.anchorY = y1
r.selection.currentX = x2
r.selection.currentY = y2
r.activeSurface = os
}
func (r *RegionSelector) getSourceBuffer(os *OutputSurface) *ShmBuffer {
if !r.showCapturedCursor && os.screenBufNoCursor != nil {
return os.screenBufNoCursor
}
return os.screenBuf
}
func (r *RegionSelector) redrawSurface(os *OutputSurface) {
srcBuf := r.getSourceBuffer(os)
if srcBuf == nil || !os.slotsReady {
return
}
slot := os.acquireFreeSlot()
if slot == nil {
return
}
slot.shm.CopyFrom(srcBuf)
// Draw overlay (dimming + selection) into this slot
r.drawOverlay(os, slot.shm)
// Attach and commit (viewport only needs to be set once, but it's cheap)
scale := os.output.scale
if scale <= 0 {
scale = 1
}
if os.viewport != nil {
srcW := float64(slot.shm.Width) / float64(scale)
srcH := float64(slot.shm.Height) / float64(scale)
_ = os.viewport.SetSource(0, 0, srcW, srcH)
_ = os.viewport.SetDestination(int32(os.logicalW), int32(os.logicalH))
}
_ = os.wlSurface.SetBufferScale(scale)
_ = os.wlSurface.Attach(slot.wlBuf, 0, 0)
_ = os.wlSurface.Damage(0, 0, int32(os.logicalW), int32(os.logicalH))
_ = os.wlSurface.Commit()
// Mark this slot as busy until compositor releases it
slot.busy = true
}
func (r *RegionSelector) cleanup() {
if r.cursorWlBuf != nil {
r.cursorWlBuf.Destroy()
}
if r.cursorPool != nil {
r.cursorPool.Destroy()
}
if r.cursorSurface != nil {
r.cursorSurface.Destroy()
}
if r.cursorBuffer != nil {
r.cursorBuffer.Close()
}
for _, os := range r.surfaces {
for _, slot := range os.slots {
if slot == nil {
continue
}
if slot.wlBuf != nil {
slot.wlBuf.Destroy()
}
if slot.pool != nil {
slot.pool.Destroy()
}
if slot.shm != nil {
slot.shm.Close()
}
}
if os.viewport != nil {
os.viewport.Destroy()
}
if os.layerSurf != nil {
os.layerSurf.Destroy()
}
if os.wlSurface != nil {
os.wlSurface.Destroy()
}
if os.screenBuf != nil {
os.screenBuf.Close()
}
if os.screenBufNoCursor != nil {
os.screenBufNoCursor.Close()
}
}
if r.shortcutsInhibitor != nil {
_ = r.shortcutsInhibitor.Destroy()
}
if r.shortcutsInhibitMgr != nil {
_ = r.shortcutsInhibitMgr.Destroy()
}
if r.viewporter != nil {
r.viewporter.Destroy()
}
if r.screencopy != nil {
r.screencopy.Destroy()
}
if r.pointer != nil {
r.pointer.Release()
}
if r.keyboard != nil {
r.keyboard.Release()
}
if r.display != nil {
r.ctx.Close()
}
}

View File

@@ -0,0 +1,264 @@
package screenshot
import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
func (r *RegionSelector) setupInput() {
if r.seat == nil {
return
}
r.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && r.pointer == nil {
if pointer, err := r.seat.GetPointer(); err == nil {
r.pointer = pointer
r.setupPointerHandlers()
}
}
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && r.keyboard == nil {
if keyboard, err := r.seat.GetKeyboard(); err == nil {
r.keyboard = keyboard
r.setupKeyboardHandlers()
}
}
})
}
func (r *RegionSelector) setupPointerHandlers() {
r.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
if r.cursorSurface != nil {
_ = r.pointer.SetCursor(e.Serial, r.cursorSurface, 12, 12)
}
r.activeSurface = nil
for _, os := range r.surfaces {
if os.wlSurface.ID() == e.Surface.ID() {
r.activeSurface = os
break
}
}
r.pointerX = e.SurfaceX
r.pointerY = e.SurfaceY
})
r.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
if r.activeSurface == nil {
return
}
r.pointerX = e.SurfaceX
r.pointerY = e.SurfaceY
if !r.selection.dragging {
return
}
curX, curY := e.SurfaceX, e.SurfaceY
if r.shiftHeld {
dx := curX - r.selection.anchorX
dy := curY - r.selection.anchorY
adx, ady := dx, dy
if adx < 0 {
adx = -adx
}
if ady < 0 {
ady = -ady
}
size := adx
if ady > adx {
size = ady
}
if dx < 0 {
curX = r.selection.anchorX - size
} else {
curX = r.selection.anchorX + size
}
if dy < 0 {
curY = r.selection.anchorY - size
} else {
curY = r.selection.anchorY + size
}
}
r.selection.currentX = curX
r.selection.currentY = curY
for _, os := range r.surfaces {
r.redrawSurface(os)
}
})
r.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
if r.activeSurface == nil {
return
}
switch e.Button {
case 0x110: // BTN_LEFT
switch e.State {
case 1: // pressed
r.preSelect = Region{}
r.selection.hasSelection = true
r.selection.dragging = true
r.selection.surface = r.activeSurface
r.selection.anchorX = r.pointerX
r.selection.anchorY = r.pointerY
r.selection.currentX = r.pointerX
r.selection.currentY = r.pointerY
for _, os := range r.surfaces {
r.redrawSurface(os)
}
case 0: // released
r.selection.dragging = false
for _, os := range r.surfaces {
r.redrawSurface(os)
}
}
default:
r.cancelled = true
r.running = false
}
})
}
func (r *RegionSelector) setupKeyboardHandlers() {
r.keyboard.SetModifiersHandler(func(e client.KeyboardModifiersEvent) {
r.shiftHeld = e.ModsDepressed&1 != 0
})
r.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
if e.State != 1 {
return
}
switch e.Key {
case 1:
r.cancelled = true
r.running = false
case 25:
r.showCapturedCursor = !r.showCapturedCursor
for _, os := range r.surfaces {
r.redrawSurface(os)
}
case 28, 57:
if r.selection.hasSelection {
r.finishSelection()
}
}
})
}
func (r *RegionSelector) finishSelection() {
if r.selection.surface == nil {
r.running = false
return
}
os := r.selection.surface
srcBuf := r.getSourceBuffer(os)
if srcBuf == nil {
r.running = false
return
}
x1, y1 := r.selection.anchorX, r.selection.anchorY
x2, y2 := r.selection.currentX, r.selection.currentY
if x1 > x2 {
x1, x2 = x2, x1
}
if y1 > y2 {
y1, y2 = y2, y1
}
scaleX, scaleY := 1.0, 1.0
if os.logicalW > 0 {
scaleX = float64(srcBuf.Width) / float64(os.logicalW)
scaleY = float64(srcBuf.Height) / float64(os.logicalH)
}
bx1 := int(x1 * scaleX)
by1 := int(y1 * scaleY)
bx2 := int(x2 * scaleX)
by2 := int(y2 * scaleY)
// Clamp to buffer bounds
if bx1 < 0 {
bx1 = 0
}
if by1 < 0 {
by1 = 0
}
if bx2 > srcBuf.Width {
bx2 = srcBuf.Width
}
if by2 > srcBuf.Height {
by2 = srcBuf.Height
}
w, h := bx2-bx1+1, by2-by1+1
if r.shiftHeld && w != h {
if w < h {
h = w
} else {
w = h
}
}
if w < 1 {
w = 1
}
if h < 1 {
h = 1
}
// Create cropped buffer and copy pixels directly
cropped, err := CreateShmBuffer(w, h, w*4)
if err != nil {
r.running = false
return
}
srcData := srcBuf.Data()
dstData := cropped.Data()
for y := 0; y < h; y++ {
srcY := by1 + y
if srcY >= srcBuf.Height {
break
}
for x := 0; x < w; x++ {
srcX := bx1 + x
if srcX >= srcBuf.Width {
break
}
si := srcY*srcBuf.Stride + srcX*4
di := y*cropped.Stride + x*4
if si+3 < len(srcData) && di+3 < len(dstData) {
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
dstData[di+3] = srcData[si+3]
}
}
}
r.capturedBuffer = cropped
r.capturedRegion = Region{
X: int32(bx1),
Y: int32(by1),
Width: int32(w),
Height: int32(h),
Output: os.output.name,
}
// Also store for "last region" feature with global coords
r.result = Region{
X: int32(bx1) + os.output.x,
Y: int32(by1) + os.output.y,
Width: int32(w),
Height: int32(h),
Output: os.output.name,
}
r.running = false
}

View File

@@ -0,0 +1,322 @@
package screenshot
import "fmt"
var fontGlyphs = map[rune][12]uint8{
'0': {0x3C, 0x66, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
'1': {0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00},
'2': {0x3C, 0x66, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x66, 0x7E, 0x00, 0x00},
'3': {0x3C, 0x66, 0x06, 0x06, 0x1C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
'4': {0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xCC, 0xFE, 0x0C, 0x0C, 0x1E, 0x00, 0x00},
'5': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
'6': {0x1C, 0x30, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
'7': {0x7E, 0x66, 0x06, 0x06, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00},
'8': {0x3C, 0x66, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
'9': {0x3C, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x0C, 0x38, 0x00, 0x00},
'x': {0x00, 0x00, 0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00, 0x00},
'E': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00, 0x00},
'P': {0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00},
'S': {0x3C, 0x66, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
'a': {0x00, 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
'c': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00, 0x00},
'd': {0x00, 0x00, 0x06, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
'e': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x7E, 0x60, 0x60, 0x3C, 0x00, 0x00},
'h': {0x00, 0x60, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00},
'i': {0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00},
'n': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00},
'o': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
'p': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x00, 0x00},
'r': {0x00, 0x00, 0x00, 0x6E, 0x76, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00},
's': {0x00, 0x00, 0x00, 0x3E, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x7C, 0x00, 0x00},
't': {0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00},
'u': {0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
'w': {0x00, 0x00, 0x00, 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00, 0x00},
'l': {0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00},
' ': {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
':': {0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00},
'/': {0x00, 0x02, 0x06, 0x0C, 0x18, 0x18, 0x30, 0x60, 0x40, 0x00, 0x00, 0x00},
'[': {0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00},
']': {0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00},
}
type OverlayStyle struct {
BackgroundR, BackgroundG, BackgroundB, BackgroundA uint8
TextR, TextG, TextB uint8
AccentR, AccentG, AccentB uint8
}
var DefaultOverlayStyle = OverlayStyle{
BackgroundR: 30, BackgroundG: 30, BackgroundB: 30, BackgroundA: 220,
TextR: 255, TextG: 255, TextB: 255,
AccentR: 100, AccentG: 180, AccentB: 255,
}
func (r *RegionSelector) drawOverlay(os *OutputSurface, renderBuf *ShmBuffer) {
data := renderBuf.Data()
stride := renderBuf.Stride
w, h := renderBuf.Width, renderBuf.Height
format := os.screenFormat
// Dim the entire buffer
for y := 0; y < h; y++ {
off := y * stride
for x := 0; x < w; x++ {
i := off + x*4
if i+3 >= len(data) {
continue
}
data[i+0] = uint8(int(data[i+0]) * 3 / 5)
data[i+1] = uint8(int(data[i+1]) * 3 / 5)
data[i+2] = uint8(int(data[i+2]) * 3 / 5)
}
}
r.drawHUD(data, stride, w, h, format)
if !r.selection.hasSelection || r.selection.surface != os {
return
}
scaleX := float64(w) / float64(os.logicalW)
scaleY := float64(h) / float64(os.logicalH)
bx1 := int(r.selection.anchorX * scaleX)
by1 := int(r.selection.anchorY * scaleY)
bx2 := int(r.selection.currentX * scaleX)
by2 := int(r.selection.currentY * scaleY)
if bx1 > bx2 {
bx1, bx2 = bx2, bx1
}
if by1 > by2 {
by1, by2 = by2, by1
}
bx1 = clamp(bx1, 0, w-1)
by1 = clamp(by1, 0, h-1)
bx2 = clamp(bx2, 0, w-1)
by2 = clamp(by2, 0, h-1)
srcBuf := r.getSourceBuffer(os)
srcData := srcBuf.Data()
for y := by1; y <= by2; y++ {
rowOff := y * stride
for x := bx1; x <= bx2; x++ {
si := y*srcBuf.Stride + x*4
di := rowOff + x*4
if si+3 >= len(srcData) || di+3 >= len(data) {
continue
}
data[di+0] = srcData[si+0]
data[di+1] = srcData[si+1]
data[di+2] = srcData[si+2]
data[di+3] = srcData[si+3]
}
}
selW, selH := bx2-bx1+1, by2-by1+1
if r.shiftHeld && selW != selH {
if selW < selH {
selH = selW
} else {
selW = selH
}
}
r.drawBorder(data, stride, w, h, bx1, by1, selW, selH, format)
r.drawDimensions(data, stride, w, h, bx1, by1, selW, selH, format)
}
func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32) {
if r.selection.dragging {
return
}
style := LoadOverlayStyle()
const charW, charH, padding, itemSpacing = 8, 12, 12, 24
cursorLabel := "hide"
if !r.showCapturedCursor {
cursorLabel = "show"
}
items := []struct{ key, desc string }{
{"Space/Enter", "capture"},
{"P", cursorLabel + " cursor"},
{"Esc", "cancel"},
}
totalW := 0
for i, item := range items {
totalW += len(item.key)*(charW+1) + 4 + len(item.desc)*(charW+1)
if i < len(items)-1 {
totalW += itemSpacing
}
}
hudW := totalW + padding*2
hudH := charH + padding*2
hudX := (bufW - hudW) / 2
hudY := bufH - hudH - 20
r.fillRect(data, stride, bufW, bufH, hudX, hudY, hudW, hudH,
style.BackgroundR, style.BackgroundG, style.BackgroundB, style.BackgroundA, format)
tx, ty := hudX+padding, hudY+padding
for i, item := range items {
r.drawText(data, stride, bufW, bufH, tx, ty, item.key,
style.AccentR, style.AccentG, style.AccentB, format)
tx += len(item.key) * (charW + 1)
r.drawText(data, stride, bufW, bufH, tx, ty, " "+item.desc,
style.TextR, style.TextG, style.TextB, format)
tx += (1 + len(item.desc)) * (charW + 1)
if i < len(items)-1 {
tx += itemSpacing
}
}
}
func (r *RegionSelector) drawBorder(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) {
const thickness = 2
for i := 0; i < thickness; i++ {
r.drawHLine(data, stride, bufW, bufH, x-i, y-i, w+2*i, format)
r.drawHLine(data, stride, bufW, bufH, x-i, y+h+i-1, w+2*i, format)
r.drawVLine(data, stride, bufW, bufH, x-i, y-i, h+2*i, format)
r.drawVLine(data, stride, bufW, bufH, x+w+i-1, y-i, h+2*i, format)
}
}
func (r *RegionSelector) drawHLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) {
if y < 0 || y >= bufH {
return
}
rowOff := y * stride
for i := 0; i < length; i++ {
px := x + i
if px < 0 || px >= bufW {
continue
}
off := rowOff + px*4
if off+3 >= len(data) {
continue
}
data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255
}
}
func (r *RegionSelector) drawVLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) {
if x < 0 || x >= bufW {
return
}
for i := 0; i < length; i++ {
py := y + i
if py < 0 || py >= bufH {
continue
}
off := py*stride + x*4
if off+3 >= len(data) {
continue
}
data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255
}
}
func (r *RegionSelector) drawDimensions(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) {
text := fmt.Sprintf("%dx%d", w, h)
const charW, charH = 8, 12
textW := len(text) * (charW + 1)
textH := charH
tx := x + (w-textW)/2
ty := y + h + 8
if ty+textH > bufH {
ty = y - textH - 8
}
tx = clamp(tx, 0, bufW-textW)
r.fillRect(data, stride, bufW, bufH, tx-4, ty-2, textW+8, textH+4, 0, 0, 0, 200, format)
r.drawText(data, stride, bufW, bufH, tx, ty, text, 255, 255, 255, format)
}
func (r *RegionSelector) fillRect(data []byte, stride, bufW, bufH, x, y, w, h int, cr, cg, cb, ca uint8, format uint32) {
alpha := float64(ca) / 255.0
invAlpha := 1.0 - alpha
c0, c2 := cb, cr
if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) {
c0, c2 = cr, cb
}
for py := y; py < y+h && py < bufH; py++ {
if py < 0 {
continue
}
for px := x; px < x+w && px < bufW; px++ {
if px < 0 {
continue
}
off := py*stride + px*4
if off+3 >= len(data) {
continue
}
data[off+0] = uint8(float64(data[off+0])*invAlpha + float64(c0)*alpha)
data[off+1] = uint8(float64(data[off+1])*invAlpha + float64(cg)*alpha)
data[off+2] = uint8(float64(data[off+2])*invAlpha + float64(c2)*alpha)
data[off+3] = 255
}
}
}
func (r *RegionSelector) drawText(data []byte, stride, bufW, bufH, x, y int, text string, cr, cg, cb uint8, format uint32) {
for i, ch := range text {
r.drawChar(data, stride, bufW, bufH, x+i*9, y, ch, cr, cg, cb, format)
}
}
func (r *RegionSelector) drawChar(data []byte, stride, bufW, bufH, x, y int, ch rune, cr, cg, cb uint8, format uint32) {
glyph, ok := fontGlyphs[ch]
if !ok {
return
}
c0, c2 := cb, cr
if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) {
c0, c2 = cr, cb
}
for row := 0; row < 12; row++ {
py := y + row
if py < 0 || py >= bufH {
continue
}
bits := glyph[row]
for col := 0; col < 8; col++ {
if (bits & (1 << (7 - col))) == 0 {
continue
}
px := x + col
if px < 0 || px >= bufW {
continue
}
off := py*stride + px*4
if off+3 >= len(data) {
continue
}
data[off], data[off+1], data[off+2], data[off+3] = c0, cg, c2, 255
}
}
}
func clamp(v, lo, hi int) int {
switch {
case v < lo:
return lo
case v > hi:
return hi
default:
return v
}
}

View File

@@ -0,0 +1,613 @@
package screenshot
import (
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type WaylandOutput struct {
wlOutput *client.Output
globalName uint32
name string
x, y int32
width int32
height int32
scale int32
transform int32
}
type CaptureResult struct {
Buffer *ShmBuffer
Region Region
YInverted bool
Format uint32
}
type Screenshoter struct {
config Config
display *client.Display
registry *client.Registry
ctx *client.Context
compositor *client.Compositor
shm *client.Shm
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
outputs map[uint32]*WaylandOutput
outputsMu sync.Mutex
}
func New(config Config) *Screenshoter {
return &Screenshoter{
config: config,
outputs: make(map[uint32]*WaylandOutput),
}
}
func (s *Screenshoter) Run() (*CaptureResult, error) {
if err := s.connect(); err != nil {
return nil, fmt.Errorf("wayland connect: %w", err)
}
defer s.cleanup()
if err := s.setupRegistry(); err != nil {
return nil, fmt.Errorf("registry setup: %w", err)
}
if err := s.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if s.screencopy == nil {
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
}
if err := s.roundtrip(); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
switch s.config.Mode {
case ModeLastRegion:
return s.captureLastRegion()
case ModeRegion:
return s.captureRegion()
case ModeWindow:
return s.captureWindow()
case ModeOutput:
return s.captureOutput(s.config.OutputName)
case ModeFullScreen:
return s.captureFullScreen()
case ModeAllScreens:
return s.captureAllScreens()
default:
return s.captureRegion()
}
}
func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
lastRegion := GetLastRegion()
if lastRegion.IsEmpty() {
return s.captureRegion()
}
output := s.findOutputForRegion(lastRegion)
if output == nil {
return s.captureRegion()
}
return s.captureRegionOnOutput(output, lastRegion)
}
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
selector := NewRegionSelector(s)
result, cancelled, err := selector.Run()
if err != nil {
return nil, fmt.Errorf("region selection: %w", err)
}
if cancelled || result == nil {
return nil, nil
}
if err := SaveLastRegion(result.Region); err != nil {
log.Debug("failed to save last region", "err", err)
}
return result, nil
}
func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
geom, err := GetActiveWindow()
if err != nil {
return nil, err
}
region := Region{
X: geom.X,
Y: geom.Y,
Width: geom.Width,
Height: geom.Height,
}
output := s.findOutputForRegion(region)
if output == nil {
return nil, fmt.Errorf("could not find output for window")
}
return s.captureRegionOnOutput(output, region)
}
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
output := s.findFocusedOutput()
if output == nil {
s.outputsMu.Lock()
for _, o := range s.outputs {
output = o
break
}
s.outputsMu.Unlock()
}
if output == nil {
return nil, fmt.Errorf("no output available")
}
return s.captureWholeOutput(output)
}
func (s *Screenshoter) captureOutput(name string) (*CaptureResult, error) {
s.outputsMu.Lock()
var output *WaylandOutput
for _, o := range s.outputs {
if o.name == name {
output = o
break
}
}
s.outputsMu.Unlock()
if output == nil {
return nil, fmt.Errorf("output %q not found", name)
}
return s.captureWholeOutput(output)
}
func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
s.outputsMu.Lock()
outputs := make([]*WaylandOutput, 0, len(s.outputs))
var minX, minY, maxX, maxY int32
first := true
for _, o := range s.outputs {
outputs = append(outputs, o)
right := o.x + o.width
bottom := o.y + o.height
if first {
minX, minY = o.x, o.y
maxX, maxY = right, bottom
first = false
continue
}
if o.x < minX {
minX = o.x
}
if o.y < minY {
minY = o.y
}
if right > maxX {
maxX = right
}
if bottom > maxY {
maxY = bottom
}
}
s.outputsMu.Unlock()
if len(outputs) == 0 {
return nil, fmt.Errorf("no outputs available")
}
if len(outputs) == 1 {
return s.captureWholeOutput(outputs[0])
}
totalW := maxX - minX
totalH := maxY - minY
compositeStride := int(totalW) * 4
composite, err := CreateShmBuffer(int(totalW), int(totalH), compositeStride)
if err != nil {
return nil, fmt.Errorf("create composite buffer: %w", err)
}
composite.Clear()
var format uint32
for _, output := range outputs {
result, err := s.captureWholeOutput(output)
if err != nil {
log.Warn("failed to capture output", "name", output.name, "err", err)
continue
}
if format == 0 {
format = result.Format
}
s.blitBuffer(composite, result.Buffer, int(output.x-minX), int(output.y-minY), result.YInverted)
result.Buffer.Close()
}
return &CaptureResult{
Buffer: composite,
Region: Region{X: minX, Y: minY, Width: totalW, Height: totalH},
Format: format,
}, nil
}
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
srcData := src.Data()
dstData := dst.Data()
for srcY := 0; srcY < src.Height; srcY++ {
actualSrcY := srcY
if yInverted {
actualSrcY = src.Height - 1 - srcY
}
dy := dstY + srcY
if dy < 0 || dy >= dst.Height {
continue
}
srcRowOff := actualSrcY * src.Stride
dstRowOff := dy * dst.Stride
for srcX := 0; srcX < src.Width; srcX++ {
dx := dstX + srcX
if dx < 0 || dx >= dst.Width {
continue
}
si := srcRowOff + srcX*4
di := dstRowOff + dx*4
if si+3 >= len(srcData) || di+3 >= len(dstData) {
continue
}
dstData[di+0] = srcData[si+0]
dstData[di+1] = srcData[si+1]
dstData[di+2] = srcData[si+2]
dstData[di+3] = srcData[si+3]
}
}
}
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
if err != nil {
return nil, fmt.Errorf("capture output: %w", err)
}
return s.processFrame(frame, Region{
X: output.x,
Y: output.y,
Width: output.width,
Height: output.height,
Output: output.name,
})
}
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
localX := region.X - output.x
localY := region.Y - output.y
cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutputRegion(
cursor,
output.wlOutput,
localX, localY,
region.Width, region.Height,
)
if err != nil {
return nil, fmt.Errorf("capture region: %w", err)
}
return s.processFrame(frame, region)
}
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
var buf *ShmBuffer
var format PixelFormat
var yInverted bool
ready := false
failed := false
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
var err error
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
if err != nil {
log.Error("failed to create buffer", "err", err)
return
}
format = PixelFormat(e.Format)
buf.Format = format
})
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
yInverted = (e.Flags & 1) != 0
})
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
if buf == nil {
return
}
pool, err := s.shm.CreatePool(buf.Fd(), int32(buf.Size()))
if err != nil {
log.Error("failed to create pool", "err", err)
return
}
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), uint32(format))
if err != nil {
pool.Destroy()
log.Error("failed to create wl_buffer", "err", err)
return
}
if err := frame.Copy(wlBuf); err != nil {
log.Error("failed to copy frame", "err", err)
}
pool.Destroy()
})
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
ready = true
})
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
failed = true
})
for !ready && !failed {
if err := s.ctx.Dispatch(); err != nil {
frame.Destroy()
return nil, fmt.Errorf("dispatch: %w", err)
}
}
frame.Destroy()
if failed {
if buf != nil {
buf.Close()
}
return nil, fmt.Errorf("frame capture failed")
}
return &CaptureResult{
Buffer: buf,
Region: region,
YInverted: yInverted,
Format: uint32(format),
}, nil
}
func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
cx := region.X + region.Width/2
cy := region.Y + region.Height/2
for _, o := range s.outputs {
if cx >= o.x && cx < o.x+o.width && cy >= o.y && cy < o.y+o.height {
return o
}
}
for _, o := range s.outputs {
if region.X >= o.x && region.X < o.x+o.width &&
region.Y >= o.y && region.Y < o.y+o.height {
return o
}
}
return nil
}
func (s *Screenshoter) findFocusedOutput() *WaylandOutput {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
for _, o := range s.outputs {
return o
}
return nil
}
func (s *Screenshoter) connect() error {
display, err := client.Connect("")
if err != nil {
return err
}
s.display = display
s.ctx = display.Context()
return nil
}
func (s *Screenshoter) roundtrip() error {
return wlhelpers.Roundtrip(s.display, s.ctx)
}
func (s *Screenshoter) setupRegistry() error {
registry, err := s.display.GetRegistry()
if err != nil {
return err
}
s.registry = registry
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
s.handleGlobal(e)
})
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
s.outputsMu.Lock()
delete(s.outputs, e.Name)
s.outputsMu.Unlock()
})
return nil
}
func (s *Screenshoter) handleGlobal(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.CompositorInterfaceName:
comp := client.NewCompositor(s.ctx)
if err := s.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
s.compositor = comp
}
case client.ShmInterfaceName:
shm := client.NewShm(s.ctx)
if err := s.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
s.shm = shm
}
case client.OutputInterfaceName:
output := client.NewOutput(s.ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := s.registry.Bind(e.Name, e.Interface, version, output); err == nil {
s.outputsMu.Lock()
s.outputs[e.Name] = &WaylandOutput{
wlOutput: output,
globalName: e.Name,
scale: 1,
}
s.outputsMu.Unlock()
s.setupOutputHandlers(e.Name, output)
}
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
sc := wlr_screencopy.NewZwlrScreencopyManagerV1(s.ctx)
version := e.Version
if version > 3 {
version = 3
}
if err := s.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
s.screencopy = sc
}
}
}
func (s *Screenshoter) setupOutputHandlers(name uint32, output *client.Output) {
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
s.outputsMu.Lock()
if o, ok := s.outputs[name]; ok {
o.x, o.y = e.X, e.Y
o.transform = int32(e.Transform)
}
s.outputsMu.Unlock()
})
output.SetModeHandler(func(e client.OutputModeEvent) {
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
return
}
s.outputsMu.Lock()
if o, ok := s.outputs[name]; ok {
o.width, o.height = e.Width, e.Height
}
s.outputsMu.Unlock()
})
output.SetScaleHandler(func(e client.OutputScaleEvent) {
s.outputsMu.Lock()
if o, ok := s.outputs[name]; ok {
o.scale = e.Factor
}
s.outputsMu.Unlock()
})
output.SetNameHandler(func(e client.OutputNameEvent) {
s.outputsMu.Lock()
if o, ok := s.outputs[name]; ok {
o.name = e.Name
}
s.outputsMu.Unlock()
})
}
func (s *Screenshoter) cleanup() {
if s.screencopy != nil {
s.screencopy.Destroy()
}
if s.display != nil {
s.ctx.Close()
}
}
func (s *Screenshoter) GetOutputs() []*WaylandOutput {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
out := make([]*WaylandOutput, 0, len(s.outputs))
for _, o := range s.outputs {
out = append(out, o)
}
return out
}
func ListOutputs() ([]Output, error) {
sc := New(DefaultConfig())
if err := sc.connect(); err != nil {
return nil, err
}
defer sc.cleanup()
if err := sc.setupRegistry(); err != nil {
return nil, err
}
if err := sc.roundtrip(); err != nil {
return nil, err
}
if err := sc.roundtrip(); err != nil {
return nil, err
}
sc.outputsMu.Lock()
defer sc.outputsMu.Unlock()
result := make([]Output, 0, len(sc.outputs))
for _, o := range sc.outputs {
result = append(result, Output{
Name: o.name,
X: o.x,
Y: o.y,
Width: o.width,
Height: o.height,
Scale: o.scale,
})
}
return result, nil
}

View File

@@ -0,0 +1,18 @@
package screenshot
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
type PixelFormat = shm.PixelFormat
const (
FormatARGB8888 = shm.FormatARGB8888
FormatXRGB8888 = shm.FormatXRGB8888
FormatABGR8888 = shm.FormatABGR8888
FormatXBGR8888 = shm.FormatXBGR8888
)
type ShmBuffer = shm.Buffer
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
return shm.CreateBuffer(width, height, stride)
}

View File

@@ -0,0 +1,65 @@
package screenshot
import (
"encoding/json"
"os"
"path"
"path/filepath"
)
type PersistentState struct {
LastRegion Region `json:"last_region"`
}
func getStateFilePath() string {
cacheDir, err := os.UserCacheDir()
if err != nil {
cacheDir = path.Join(os.Getenv("HOME"), ".cache")
}
return filepath.Join(cacheDir, "dms", "screenshot-state.json")
}
func LoadState() (*PersistentState, error) {
path := getStateFilePath()
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return &PersistentState{}, nil
}
return nil, err
}
var state PersistentState
if err := json.Unmarshal(data, &state); err != nil {
return &PersistentState{}, nil
}
return &state, nil
}
func SaveState(state *PersistentState) error {
path := getStateFilePath()
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
func GetLastRegion() Region {
state, err := LoadState()
if err != nil {
return Region{}
}
return state.LastRegion
}
func SaveLastRegion(r Region) error {
state, _ := LoadState()
state.LastRegion = r
return SaveState(state)
}

View File

@@ -0,0 +1,127 @@
package screenshot
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
)
type ThemeColors struct {
Background string `json:"surface"`
OnSurface string `json:"on_surface"`
Primary string `json:"primary"`
}
type ColorScheme struct {
Dark ThemeColors `json:"dark"`
Light ThemeColors `json:"light"`
}
type ColorsFile struct {
Colors ColorScheme `json:"colors"`
}
var cachedStyle *OverlayStyle
func LoadOverlayStyle() OverlayStyle {
if cachedStyle != nil {
return *cachedStyle
}
style := DefaultOverlayStyle
colors := loadColorsFile()
if colors == nil {
cachedStyle = &style
return style
}
theme := &colors.Dark
if isLightMode() {
theme = &colors.Light
}
if bg, ok := parseHexColor(theme.Background); ok {
style.BackgroundR, style.BackgroundG, style.BackgroundB = bg[0], bg[1], bg[2]
}
if text, ok := parseHexColor(theme.OnSurface); ok {
style.TextR, style.TextG, style.TextB = text[0], text[1], text[2]
}
if accent, ok := parseHexColor(theme.Primary); ok {
style.AccentR, style.AccentG, style.AccentB = accent[0], accent[1], accent[2]
}
cachedStyle = &style
return style
}
func loadColorsFile() *ColorScheme {
path := getColorsFilePath()
data, err := os.ReadFile(path)
if err != nil {
return nil
}
var file ColorsFile
if err := json.Unmarshal(data, &file); err != nil {
return nil
}
return &file.Colors
}
func getColorsFilePath() string {
cacheDir := os.Getenv("XDG_CACHE_HOME")
if cacheDir == "" {
home := os.Getenv("HOME")
if home == "" {
return ""
}
cacheDir = filepath.Join(home, ".cache")
}
return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json")
}
func isLightMode() bool {
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
if err != nil {
return false
}
scheme := strings.TrimSpace(string(out))
switch scheme {
case "'prefer-light'", "'default'":
return true
}
return false
}
func parseHexColor(hex string) ([3]uint8, bool) {
hex = strings.TrimPrefix(hex, "#")
if len(hex) != 6 {
return [3]uint8{}, false
}
var r, g, b uint8
for i, ptr := range []*uint8{&r, &g, &b} {
val := 0
for j := 0; j < 2; j++ {
c := hex[i*2+j]
val *= 16
switch {
case c >= '0' && c <= '9':
val += int(c - '0')
case c >= 'a' && c <= 'f':
val += int(c - 'a' + 10)
case c >= 'A' && c <= 'F':
val += int(c - 'A' + 10)
default:
return [3]uint8{}, false
}
}
*ptr = uint8(val)
}
return [3]uint8{r, g, b}, true
}

View File

@@ -0,0 +1,68 @@
package screenshot
type Mode int
const (
ModeRegion Mode = iota
ModeWindow
ModeFullScreen
ModeAllScreens
ModeOutput
ModeLastRegion
)
type Format int
const (
FormatPNG Format = iota
FormatJPEG
FormatPPM
)
type Region struct {
X int32 `json:"x"`
Y int32 `json:"y"`
Width int32 `json:"width"`
Height int32 `json:"height"`
Output string `json:"output,omitempty"`
}
func (r Region) IsEmpty() bool {
return r.Width <= 0 || r.Height <= 0
}
type Output struct {
Name string
X, Y int32
Width int32
Height int32
Scale int32
}
type Config struct {
Mode Mode
OutputName string
IncludeCursor bool
Format Format
Quality int
OutputDir string
Filename string
Clipboard bool
SaveFile bool
Notify bool
Stdout bool
}
func DefaultConfig() Config {
return Config{
Mode: ModeRegion,
IncludeCursor: false,
Format: FormatPNG,
Quality: 90,
OutputDir: "",
Filename: "",
Clipboard: true,
SaveFile: true,
Notify: true,
}
}

View File

@@ -40,6 +40,10 @@ func (b *DDCBackend) scanI2CDevices() error {
return b.scanI2CDevicesInternal(false)
}
func (b *DDCBackend) ForceRescan() error {
return b.scanI2CDevicesInternal(true)
}
func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
b.scanMutex.Lock()
defer b.scanMutex.Unlock()
@@ -64,10 +68,6 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
activeBuses[i] = true
id := fmt.Sprintf("ddc:i2c-%d", i)
if _, exists := b.devices.Load(id); exists {
continue
}
dev, err := b.probeDDCDevice(i)
if err != nil || dev == nil {
continue
@@ -261,8 +261,16 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
if _, err := os.Stat(busPath); os.IsNotExist(err) {
b.devices.Delete(id)
log.Debugf("removed stale DDC device %s (bus no longer exists)", id)
return fmt.Errorf("device disconnected: %s", id)
}
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
if err != nil {
b.devices.Delete(id)
log.Debugf("removed DDC device %s (open failed: %v)", id, err)
return fmt.Errorf("open i2c device: %w", err)
}
defer syscall.Close(fd)

View File

@@ -89,6 +89,13 @@ func (m *Manager) initDDC() {
func (m *Manager) Rescan() {
log.Debug("Rescanning brightness devices...")
if m.ddcReady && m.ddcBackend != nil {
if err := m.ddcBackend.ForceRescan(); err != nil {
log.Debugf("DDC force rescan failed: %v", err)
}
}
m.updateState()
}

View File

@@ -5,13 +5,18 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/pilebones/go-udev/netlink"
)
type UdevMonitor struct {
stop chan struct{}
stop chan struct{}
rescanMutex sync.Mutex
rescanTimer *time.Timer
rescanPending bool
}
func NewUdevMonitor(manager *Manager) *UdevMonitor {
@@ -34,10 +39,8 @@ func (m *UdevMonitor) run(manager *Manager) {
matcher := &netlink.RuleDefinitions{
Rules: []netlink.RuleDefinition{
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
// ! TODO: most drivers dont emit this for leds?
// ! inotify brightness_hw_changed works, but thn some devices dont do that...
// ! So for now the GUI just shows OSDs for leds, without reflecting actual HW value
// {Env: map[string]string{"SUBSYSTEM": "leds"}},
{Env: map[string]string{"SUBSYSTEM": "drm"}},
{Env: map[string]string{"SUBSYSTEM": "i2c"}},
},
}
if err := matcher.Compile(); err != nil {
@@ -49,7 +52,7 @@ func (m *UdevMonitor) run(manager *Manager) {
errs := make(chan error)
conn.Monitor(events, errs, matcher)
log.Info("Udev monitor started for backlight/leds events")
log.Info("Udev monitor started for backlight/drm/i2c events")
for {
select {
@@ -75,11 +78,54 @@ func (m *UdevMonitor) handleEvent(manager *Manager, event netlink.UEvent) {
sysname := filepath.Base(devpath)
action := string(event.Action)
switch subsystem {
case "drm", "i2c":
m.handleDisplayEvent(manager, action, subsystem, sysname)
case "backlight":
m.handleBacklightEvent(manager, action, sysname)
}
}
func (m *UdevMonitor) handleDisplayEvent(manager *Manager, action, subsystem, sysname string) {
switch action {
case "add", "remove", "change":
log.Debugf("Udev %s event: %s:%s - queueing DDC rescan", action, subsystem, sysname)
m.debouncedRescan(manager)
}
}
func (m *UdevMonitor) debouncedRescan(manager *Manager) {
m.rescanMutex.Lock()
defer m.rescanMutex.Unlock()
m.rescanPending = true
if m.rescanTimer != nil {
m.rescanTimer.Reset(2 * time.Second)
return
}
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
m.rescanMutex.Lock()
pending := m.rescanPending
m.rescanPending = false
m.rescanMutex.Unlock()
if !pending {
return
}
log.Debug("Executing debounced DDC rescan")
manager.Rescan()
})
}
func (m *UdevMonitor) handleBacklightEvent(manager *Manager, action, sysname string) {
switch action {
case "change":
m.handleChange(manager, subsystem, sysname)
m.handleChange(manager, "backlight", sysname)
case "add", "remove":
log.Debugf("Udev %s event: %s:%s - triggering rescan", action, subsystem, sysname)
log.Debugf("Udev %s event: backlight:%s - triggering rescan", action, sysname)
manager.Rescan()
}
}

View File

@@ -0,0 +1,26 @@
package client
import wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
func Roundtrip(display *wlclient.Display, ctx *wlclient.Context) error {
callback, err := display.Sync()
if err != nil {
return err
}
done := make(chan struct{})
callback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
close(done)
})
for {
select {
case <-done:
return nil
default:
if err := ctx.Dispatch(); err != nil {
return err
}
}
}
}

View File

@@ -0,0 +1,139 @@
package shm
import (
"fmt"
"golang.org/x/sys/unix"
)
type PixelFormat uint32
const (
FormatARGB8888 PixelFormat = 0
FormatXRGB8888 PixelFormat = 1
FormatABGR8888 PixelFormat = 0x34324241
FormatXBGR8888 PixelFormat = 0x34324258
)
type Buffer struct {
fd int
data []byte
size int
Width int
Height int
Stride int
Format PixelFormat
}
func CreateBuffer(width, height, stride int) (*Buffer, error) {
size := stride * height
fd, err := unix.MemfdCreate("dms-shm", 0)
if err != nil {
return nil, fmt.Errorf("memfd_create: %w", err)
}
if err := unix.Ftruncate(fd, int64(size)); err != nil {
unix.Close(fd)
return nil, fmt.Errorf("ftruncate: %w", err)
}
data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
unix.Close(fd)
return nil, fmt.Errorf("mmap: %w", err)
}
return &Buffer{
fd: fd,
data: data,
size: size,
Width: width,
Height: height,
Stride: stride,
}, nil
}
func (b *Buffer) Fd() int { return b.fd }
func (b *Buffer) Size() int { return b.size }
func (b *Buffer) Data() []byte { return b.data }
func (b *Buffer) Close() error {
var firstErr error
if b.data != nil {
if err := unix.Munmap(b.data); err != nil && firstErr == nil {
firstErr = fmt.Errorf("munmap: %w", err)
}
b.data = nil
}
if b.fd >= 0 {
if err := unix.Close(b.fd); err != nil && firstErr == nil {
firstErr = fmt.Errorf("close: %w", err)
}
b.fd = -1
}
return firstErr
}
func (b *Buffer) GetPixelRGBA(x, y int) (r, g, b2, a uint8) {
if x < 0 || x >= b.Width || y < 0 || y >= b.Height {
return
}
off := y*b.Stride + x*4
if off+3 >= len(b.data) {
return
}
return b.data[off+2], b.data[off+1], b.data[off], b.data[off+3]
}
func (b *Buffer) GetPixelBGRA(x, y int) (b2, g, r, a uint8) {
if x < 0 || x >= b.Width || y < 0 || y >= b.Height {
return
}
off := y*b.Stride + x*4
if off+3 >= len(b.data) {
return
}
return b.data[off], b.data[off+1], b.data[off+2], b.data[off+3]
}
func (b *Buffer) ConvertBGRAtoRGBA() {
for y := 0; y < b.Height; y++ {
rowOff := y * b.Stride
for x := 0; x < b.Width; x++ {
off := rowOff + x*4
if off+3 >= len(b.data) {
continue
}
b.data[off], b.data[off+2] = b.data[off+2], b.data[off]
}
}
}
func (b *Buffer) FlipVertical() {
tmp := make([]byte, b.Stride)
for y := 0; y < b.Height/2; y++ {
topOff := y * b.Stride
botOff := (b.Height - 1 - y) * b.Stride
copy(tmp, b.data[topOff:topOff+b.Stride])
copy(b.data[topOff:topOff+b.Stride], b.data[botOff:botOff+b.Stride])
copy(b.data[botOff:botOff+b.Stride], tmp)
}
}
func (b *Buffer) Clear() {
for i := range b.data {
b.data[i] = 0
}
}
func (b *Buffer) CopyFrom(src *Buffer) {
copy(b.data, src.data)
}

View File

@@ -0,0 +1,13 @@
package xdg_shell
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
type Popup struct {
client.BaseProxy
}
func NewPopup(ctx *client.Context) *Popup {
p := &Popup{}
ctx.Register(p)
return p
}

View File

@@ -1,12 +1,12 @@
dms-git (0.6.2+git5) nightly; urgency=medium
dms-git (0.6.2+git2264.c5c5ce84) nightly; urgency=medium
* Add VERSION file creation to all distro packages
* Fix Fedora COPR to pass VERSION/COMMIT to make dist
* Fix obs-upload.sh auto-increment to preserve git hash and add ppa suffix
* Fix debian/rules to use source at root level (native format)
* Remove incorrect dms-git-source subdirectory references
* Fix Build-Depends parsing path issue in obs-upload.sh auto-increment
* Fix Build-Depends parsing in obs-upload.sh for proper dependency extraction
* Build dms binary from source for true git version strings
* Match Fedora COPR git build behavior
* Now shows proper git version (e.g., v0.6.2-11-g12e91534)
* Add golang-go and make as build dependencies
-- Avenge Media <AvengeMedia.US@gmail.com> Wed, 27 Nov 2025 04:15:00 -0500
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 03 Dec 2025 01:50:00 +0000

View File

@@ -27,12 +27,11 @@ override_dh_auto_build:
# Patch go.mod to use Go 1.24 base version (Debian 13 has 1.23.x, may vary)
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
# Extract version info for embedding
VERSION="$(UPSTREAM_VERSION)"
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown")
# Build dms-cli from source using vendored dependencies
# Extract version info and build in single shell to preserve variables
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
VERSION="$(UPSTREAM_VERSION)"; \
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown"); \
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
MAKE_ARCH=amd64; \
BINARY_NAME=dms-linux-amd64; \
@@ -71,6 +70,8 @@ override_dh_auto_install:
rm -rf debian/dms-git/usr/share/quickshell/dms/core \
debian/dms-git/usr/share/quickshell/dms/distro
echo "$(UPSTREAM_VERSION)" > debian/dms-git/usr/share/quickshell/dms/VERSION
override_dh_auto_clean:
# Clean up build artifacts
rm -f dms

View File

@@ -33,7 +33,6 @@ Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen
Recommends: quickshell-git
Recommends: wl-clipboard
@@ -98,8 +97,11 @@ chmod +x %{_builddir}/dgop
%build
# Build DMS CLI from source (core/subdirectory)
VERSION="%{version}"
COMMIT=$(echo "%{version}" | grep -oP '[a-f0-9]{7,}' | head -n1 || echo "unknown")
cd core
make dist
make dist VERSION="$VERSION" COMMIT="$COMMIT"
%install
# Install dms-cli binary (built from source)
@@ -132,11 +134,10 @@ install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
# Install systemd user service
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
# Install desktop file and icon
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
# Install shell files to shared data location (from quickshell/ subdirectory)
# Install shell files to shared data location
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r quickshell/* %{buildroot}%{_datadir}/quickshell/dms/
@@ -146,13 +147,9 @@ rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
# Clean up old installation path from previous versions
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
# Restart DMS for active users after upgrade
if [ "$1" -ge 2 ]; then

View File

@@ -7,7 +7,7 @@
}: let
cfg = config.programs.dankMaterialShell;
in {
qmlPath = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
qmlPath = "${dmsPkgs.dms-shell}/share/quickshell/dms";
packages =
[
@@ -19,7 +19,7 @@ in {
pkgs.libsForQt5.qt5ct
pkgs.kdePackages.qt6ct
dmsPkgs.dmsCli
dmsPkgs.dms-shell
]
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
++ lib.optionals cfg.enableClipboard [pkgs.cliphist pkgs.wl-clipboard]

View File

@@ -20,7 +20,7 @@
"--command"
cfg.compositor.name
"-p"
"${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms"
"${dmsPkgs.dms-shell}/share/quickshell/dms"
]
++ lib.optionals (cfg.compositor.customConfig != "") [
"-C"

View File

@@ -66,7 +66,7 @@ in {
};
Service = {
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
Restart = "on-failure";
};

View File

@@ -14,7 +14,7 @@ in {
config = lib.mkIf cfg.enable
{
environment.etc."xdg/quickshell/dms".source = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
environment.etc."xdg/quickshell/dms".source = "${dmsPkgs.dms-shell}/share/quickshell/dms";
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
description = "DankMaterialShell";
@@ -26,7 +26,7 @@ in {
restartTriggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
serviceConfig = {
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
Restart = "on-failure";
};
};

View File

@@ -103,6 +103,8 @@ rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true

View File

@@ -77,12 +77,9 @@ rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
%posttrans
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
pkill -USR1 -x dms >/dev/null 2>&1 || true

View File

@@ -169,6 +169,7 @@ fi
CHANGELOG_VERSION=""
if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
# Format: 0.6.2+git{COMMIT_COUNT}.{COMMIT_HASH} (e.g., 0.6.2+git2256.9162e314)
CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" "distro/debian/$PACKAGE/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "")
if [[ -n "$CHANGELOG_VERSION" ]] && [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
SOURCE_FORMAT_CHECK=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
@@ -618,15 +619,15 @@ fi
# Only auto-increment on manual runs (REBUILD_RELEASE set or not in CI), not automated workflows
OLD_DSC_FILE=""
if [[ -f "$WORK_DIR/$PACKAGE.dsc" ]]; then
OLD_DSC_FILE="$WORK_DIR/$PACKAGE.dsc"
elif [[ -f "$WORK_DIR/.osc/sources/$PACKAGE.dsc" ]]; then
if [[ -f "$WORK_DIR/.osc/sources/$PACKAGE.dsc" ]]; then
OLD_DSC_FILE="$WORK_DIR/.osc/sources/$PACKAGE.dsc"
elif [[ -f "$WORK_DIR/.osc/$PACKAGE.dsc" ]]; then
OLD_DSC_FILE="$WORK_DIR/.osc/$PACKAGE.dsc"
fi
if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[ -n "$OLD_DSC_FILE" ]]; then
OLD_DSC_VERSION=$(grep "^Version:" "$OLD_DSC_FILE" 2>/dev/null | awk '{print $2}' | head -1)
IS_MANUAL=false
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
IS_MANUAL=true
@@ -638,20 +639,17 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[
IS_MANUAL=true
echo "==> Local/manual run detected (not in CI)"
fi
if [[ -n "$OLD_DSC_VERSION" ]] && [[ "$OLD_DSC_VERSION" == "$CHANGELOG_VERSION" ]] && [[ "$IS_MANUAL" == true ]]; then
echo "==> Detected rebuild of same version $CHANGELOG_VERSION, incrementing version"
CHANGELOG_BASE=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
OLD_DSC_BASE=$(echo "$OLD_DSC_VERSION" | sed 's/ppa[0-9]*$//')
if [[ -n "$OLD_DSC_VERSION" ]] && [[ "$OLD_DSC_BASE" == "$CHANGELOG_BASE" ]] && [[ "$IS_MANUAL" == true ]]; then
echo "==> Detected rebuild of same base version $CHANGELOG_BASE, incrementing version"
if [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
NEW_VERSION="${BASE_VERSION}+git1"
echo " Incrementing git number: $CHANGELOG_VERSION -> $NEW_VERSION"
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git([0-9]+)$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
GIT_NUM="${BASH_REMATCH[2]}"
NEW_GIT_NUM=$((GIT_NUM + 1))
NEW_VERSION="${BASE_VERSION}+git${NEW_GIT_NUM}"
echo " Incrementing git number: $CHANGELOG_VERSION -> $NEW_VERSION"
NEW_VERSION="${BASE_VERSION}+gitppa1"
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)ppa([0-9]+)$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
PPA_NUM="${BASH_REMATCH[2]}"
@@ -663,7 +661,14 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[
GIT_NUM="${BASH_REMATCH[2]}"
GIT_HASH="${BASH_REMATCH[3]}"
PPA_NUM="${BASH_REMATCH[5]}"
if [[ -n "$PPA_NUM" ]]; then
# Check if old DSC has ppa suffix even if changelog doesn't
if [[ -z "$PPA_NUM" ]] && [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
OLD_PPA_NUM="${BASH_REMATCH[1]}"
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
elif [[ -n "$PPA_NUM" ]]; then
NEW_PPA_NUM=$((PPA_NUM + 1))
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"

View File

@@ -1,5 +1,8 @@
dms-git (0.6.2+git2169.f7f1bbbdppa10) questing; urgency=medium
dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
* Git snapshot (commit 2169: f7f1bbbd)
* Add VERSION file creation to all distro packages
* Fix Fedora COPR to pass VERSION/COMMIT to make dist
* Fix obs-upload.sh auto-increment to preserve git hash and add ppa suffix
* Git snapshot (commit 2264: c5c5ce84)
-- Avenge Media <AvengeMedia.US@gmail.com> Wed, 26 Nov 2025 21:43:12 -0500
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 03 Dec 2025 01:50:00 +0000

View File

@@ -62,6 +62,14 @@ override_dh_auto_install:
install -Dm644 dms-git-repo/assets/danklogo.svg \
debian/dms-git/usr/share/icons/hicolor/scalable/apps/danklogo.svg
# Create DMS Version file
@if [ -f dms-git-repo/.dms-version ]; then \
. dms-git-repo/.dms-version; \
echo "$$VERSION" > debian/dms-git/usr/share/quickshell/dms/VERSION; \
else \
echo "dev" > debian/dms-git/usr/share/quickshell/dms/VERSION; \
fi
override_dh_auto_clean:
# Don't delete dms-git-repo directory - it's part of the source package (native format)
# Clean up build artifacts

View File

@@ -54,6 +54,9 @@ override_dh_auto_install:
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/danklogo.svg \
debian/dms/usr/share/icons/hicolor/scalable/apps/danklogo.svg
# Create VERSION file for Quickshell (stable release format)
echo "$(BASE_VERSION)" > debian/dms/usr/share/quickshell/dms/VERSION
# Generate and install shell completions (if applicable)
# Uncomment if dms supports completion generation
# ./dms completion bash > dms.bash

View File

@@ -20,7 +20,7 @@
system: fn system nixpkgs.legacyPackages.${system}
);
buildDmsPkgs = pkgs: {
inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) dmsCli dankMaterialShell;
dms-shell = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
dgop = dgop.packages.${pkgs.stdenv.hostPlatform.system}.dgop;
};
mkModuleWithDmsPkgs = path: args @ {pkgs, ...}: {
@@ -46,10 +46,11 @@
+ "_"
+ (self.shortRev or "dirty");
in {
dmsCli = pkgs.buildGoModule (finalAttrs: {
dms-shell = pkgs.buildGoModule (let
rootSrc = ./.;
in {
inherit version;
pname = "dmsCli";
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-2PCqiW4frxME8IlmwWH5ktznhd/G1bah5Ae4dp0HPTQ=";
@@ -58,39 +59,56 @@
ldflags = [
"-s"
"-w"
"-X main.Version=${finalAttrs.version}"
"-X main.Version=${version}"
];
nativeBuildInputs = [pkgs.installShellFiles];
nativeBuildInputs = [
pkgs.installShellFiles
pkgs.makeWrapper
];
postInstall = ''
mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms --add-flags "-c $out/share/quickshell/dms"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish ) \
--fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh)
'';
meta = {
description = "DankMaterialShell Command Line Interface";
homepage = "https://github.com/AvengeMedia/danklinux";
mainProgram = "dms";
description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit;
platforms = pkgs.lib.platforms.unix;
mainProgram = "dms";
platforms = pkgs.lib.platforms.linux;
};
});
dankMaterialShell = pkgs.stdenvNoCC.mkDerivation {
inherit version;
pname = "dankMaterialShell";
src = ./quickshell;
installPhase = ''
mkdir -p $out/etc/xdg/quickshell
cp -r ./ $out/etc/xdg/quickshell/dms
'';
};
default = self.packages.${system}.dmsCli;
default = self.packages.${system}.dms-shell;
}
);

View File

@@ -1,373 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Services
import qs.Widgets
Singleton {
id: root
property var activeModal: null
property bool windowsVisible: false
property var targetScreen: null
readonly property bool hasActiveModal: activeModal !== null
readonly property bool shouldShowModal: hasActiveModal
readonly property var screen: backgroundWindow.screen
readonly property real dpr: screen ? CompositorService.getScreenScale(screen) : 1
readonly property real shadowBuffer: 5
property bool wantsToHide: false
property real cachedModalWidth: 400
property real cachedModalHeight: 300
property real cachedModalX: 0
property real cachedModalY: 0
property var cachedModal: null
property int cachedAnimationDuration: Theme.shortDuration
property var cachedEnterCurve: Theme.expressiveCurves.expressiveFastSpatial
property var cachedExitCurve: Theme.expressiveCurves.expressiveFastSpatial
property real cachedScaleCollapsed: 0.96
readonly property real modalWidth: cachedModalWidth
readonly property real modalHeight: cachedModalHeight
readonly property real modalX: cachedModalX
readonly property real modalY: cachedModalY
Connections {
target: root.cachedModal
function onModalWidthChanged() {
if (!root.hasActiveModal)
return;
root.cachedModalWidth = Theme.px(root.cachedModal.modalWidth, root.dpr);
root.cachedModalX = root.calculateX(root.cachedModal);
}
function onModalHeightChanged() {
if (!root.hasActiveModal)
return;
root.cachedModalHeight = Theme.px(root.cachedModal.modalHeight, root.dpr);
root.cachedModalY = root.calculateY(root.cachedModal);
}
}
onScreenChanged: {
if (!cachedModal || !screen)
return;
cachedModalWidth = Theme.px(cachedModal.modalWidth, dpr);
cachedModalHeight = Theme.px(cachedModal.modalHeight, dpr);
cachedModalX = calculateX(cachedModal);
cachedModalY = calculateY(cachedModal);
}
function showModal(modal) {
wantsToHide = false;
targetScreen = CompositorService.focusedScreen;
activeModal = modal;
cachedModal = modal;
windowsVisible = true;
cachedModalWidth = Theme.px(modal.modalWidth, dpr);
cachedModalHeight = Theme.px(modal.modalHeight, dpr);
cachedModalX = calculateX(modal);
cachedModalY = calculateY(modal);
cachedAnimationDuration = modal.animationDuration ?? Theme.shortDuration;
cachedEnterCurve = modal.animationEnterCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedExitCurve = modal.animationExitCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
cachedScaleCollapsed = modal.animationScaleCollapsed ?? 0.96;
if (modal.directContent)
Qt.callLater(focusDirectContent);
}
function focusDirectContent() {
if (!hasActiveModal)
return;
if (!cachedModal?.directContent)
return;
cachedModal.directContent.forceActiveFocus();
}
function hideModal() {
wantsToHide = true;
Qt.callLater(completeHide);
}
function completeHide() {
if (!wantsToHide)
return;
activeModal = null;
wantsToHide = false;
}
function hideModalInstant() {
wantsToHide = false;
activeModal = null;
windowsVisible = false;
cleanupInputMethod();
}
function onCloseAnimationFinished() {
if (hasActiveModal)
return;
if (cachedModal && typeof cachedModal.onFullyClosed === "function")
cachedModal.onFullyClosed();
cleanupInputMethod();
windowsVisible = false;
}
function cleanupInputMethod() {
if (!Qt.inputMethod)
return;
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
function calculateX(m) {
const screen = backgroundWindow.screen;
if (!screen)
return 0;
const w = Theme.px(m.modalWidth, dpr);
switch (m.positioning) {
case "center":
return Theme.snap((screen.width - w) / 2, dpr);
case "top-right":
return Theme.snap(Math.max(Theme.spacingL, screen.width - w - Theme.spacingL), dpr);
case "custom":
return Theme.snap(m.customPosition.x, dpr);
default:
return 0;
}
}
function calculateY(m) {
const screen = backgroundWindow.screen;
if (!screen)
return 0;
const h = Theme.px(m.modalHeight, dpr);
switch (m.positioning) {
case "center":
return Theme.snap((screen.height - h) / 2, dpr);
case "top-right":
return Theme.snap(Theme.barHeight + Theme.spacingXS, dpr);
case "custom":
return Theme.snap(m.customPosition.y, dpr);
default:
return 0;
}
}
PanelWindow {
id: backgroundWindow
visible: root.windowsVisible
screen: root.targetScreen
color: "transparent"
WlrLayershell.namespace: "dms:modal:background"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: backgroundMaskRect
intersection: Intersection.Xor
}
Item {
id: backgroundMaskRect
x: root.shouldShowModal ? root.modalX : 0
y: root.shouldShowModal ? root.modalY : 0
width: root.shouldShowModal ? root.modalWidth : (backgroundWindow.screen?.width ?? 1920)
height: root.shouldShowModal ? root.modalHeight : (backgroundWindow.screen?.height ?? 1080)
}
MouseArea {
anchors.fill: parent
enabled: root.windowsVisible
onClicked: mouse => {
if (!root.cachedModal || !root.shouldShowModal)
return;
if (!(root.cachedModal.closeOnBackgroundClick ?? true))
return;
const outside = mouse.x < root.modalX || mouse.x > root.modalX + root.modalWidth || mouse.y < root.modalY || mouse.y > root.modalY + root.modalHeight;
if (!outside)
return;
root.cachedModal.backgroundClicked();
}
}
Rectangle {
anchors.fill: parent
color: "black"
opacity: root.shouldShowModal && SettingsData.modalDarkenBackground ? (root.cachedModal?.backgroundOpacity ?? 0.5) : 0
visible: SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
}
}
}
}
PanelWindow {
id: contentWindow
visible: root.windowsVisible
screen: root.targetScreen
color: "transparent"
WlrLayershell.namespace: root.cachedModal?.layerNamespace ?? "dms:modal"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!root.hasActiveModal)
return WlrKeyboardFocus.None;
if (root.cachedModal?.customKeyboardFocus !== null && root.cachedModal?.customKeyboardFocus !== undefined)
return root.cachedModal.customKeyboardFocus;
if (CompositorService.isHyprland)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.modalX - root.shadowBuffer, root.dpr))
top: Math.max(0, Theme.snap(root.modalY - root.shadowBuffer, root.dpr))
}
implicitWidth: root.modalWidth + (root.shadowBuffer * 2)
implicitHeight: root.modalHeight + (root.shadowBuffer * 2)
mask: Region {
item: contentMaskRect
}
Item {
id: contentMaskRect
x: root.shadowBuffer
y: root.shadowBuffer
width: root.shouldShowModal ? root.modalWidth : 0
height: root.shouldShowModal ? root.modalHeight : 0
}
HyprlandFocusGrab {
windows: [contentWindow]
active: CompositorService.isHyprland && root.hasActiveModal && (root.cachedModal?.shouldHaveFocus ?? false)
}
Item {
id: contentContainer
x: root.shadowBuffer
y: root.shadowBuffer
width: root.modalWidth
height: root.modalHeight
readonly property bool hasDirectContent: root.cachedModal ? (root.cachedModal.directContent !== null && root.cachedModal.directContent !== undefined) : false
opacity: root.shouldShowModal ? 1 : 0
scale: root.shouldShowModal ? 1 : root.cachedScaleCollapsed
onHasDirectContentChanged: {
if (!hasDirectContent)
return;
const dc = root.cachedModal.directContent;
if (dc.parent === directContentWrapper)
return;
dc.parent = directContentWrapper;
dc.anchors.fill = directContentWrapper;
}
Behavior on opacity {
NumberAnimation {
id: opacityAnimation
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
onRunningChanged: {
if (running || root.shouldShowModal)
return;
root.onCloseAnimationFinished();
}
}
}
Behavior on scale {
NumberAnimation {
id: scaleAnimation
duration: root.cachedAnimationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldShowModal ? root.cachedEnterCurve : root.cachedExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.cachedModal?.backgroundColor ?? Theme.surfaceContainer
borderColor: root.cachedModal?.borderColor ?? Theme.outlineMedium
borderWidth: root.cachedModal?.borderWidth ?? 1
radius: root.cachedModal?.cornerRadius ?? Theme.cornerRadius
z: -1
}
FocusScope {
id: modalFocusScope
anchors.fill: parent
focus: root.hasActiveModal
Keys.onEscapePressed: event => {
if (!root.cachedModal?.closeOnEscapeKey)
return;
root.cachedModal.close();
event.accepted = true;
}
Keys.forwardTo: contentContainer.hasDirectContent ? [directContentWrapper] : (contentLoader.item ? [contentLoader.item] : [])
Item {
id: directContentWrapper
anchors.fill: parent
visible: contentContainer.hasDirectContent
focus: contentContainer.hasDirectContent && root.hasActiveModal
}
Loader {
id: contentLoader
anchors.fill: parent
active: !contentContainer.hasDirectContent && root.windowsVisible
asynchronous: false
sourceComponent: root.cachedModal?.content ?? null
visible: !contentContainer.hasDirectContent
focus: !contentContainer.hasDirectContent && root.hasActiveModal
onLoaded: {
if (!item)
return;
if (root.cachedModal)
root.cachedModal.loadedContent = item;
if (root.hasActiveModal)
item.forceActiveFocus();
}
onActiveChanged: {
if (active || !root.cachedModal)
return;
root.cachedModal.loadedContent = null;
}
}
}
}
}
}

View File

@@ -0,0 +1,144 @@
.pragma library
const KEY_MAP = {
16777234: "Left",
16777236: "Right",
16777235: "Up",
16777237: "Down",
44: "Comma",
46: "Period",
47: "Slash",
59: "Semicolon",
39: "Apostrophe",
91: "BracketLeft",
93: "BracketRight",
92: "Backslash",
45: "Minus",
61: "Equal",
96: "grave",
32: "space",
16777225: "Print",
16777226: "Print",
16777220: "Return",
16777221: "Return",
16777217: "Tab",
16777219: "BackSpace",
16777223: "Delete",
16777222: "Insert",
16777232: "Home",
16777233: "End",
16777238: "Page_Up",
16777239: "Page_Down",
16777216: "Escape",
16777252: "Caps_Lock",
16777253: "Num_Lock",
16777254: "Scroll_Lock",
16777224: "Pause",
16777330: "XF86AudioRaiseVolume",
16777328: "XF86AudioLowerVolume",
16777329: "XF86AudioMute",
16842808: "XF86AudioMicMute",
16777344: "XF86AudioPlay",
16777345: "XF86AudioPause",
16777346: "XF86AudioStop",
16777347: "XF86AudioNext",
16777348: "XF86AudioPrev",
16842792: "XF86AudioRecord",
16842798: "XF86MonBrightnessUp",
16842797: "XF86MonBrightnessDown",
16842800: "XF86KbdBrightnessUp",
16842799: "XF86KbdBrightnessDown",
16842796: "XF86PowerOff",
16842803: "XF86Sleep",
16842804: "XF86WakeUp",
16842802: "XF86Eject",
16842791: "XF86Calculator",
16842806: "XF86Explorer",
16842794: "XF86HomePage",
16777426: "XF86Search",
16777427: "XF86Mail",
16777442: "XF86Launch0",
16777443: "XF86Launch1",
33: "1",
64: "2",
35: "3",
36: "4",
37: "5",
94: "6",
38: "7",
42: "8",
40: "9",
41: "0",
60: "Comma",
62: "Period",
63: "Slash",
58: "Semicolon",
34: "Apostrophe",
123: "BracketLeft",
125: "BracketRight",
124: "Backslash",
95: "Minus",
43: "Equal",
126: "grave"
};
function xkbKeyFromQtKey(qk) {
if (qk >= 65 && qk <= 90)
return String.fromCharCode(qk);
if (qk >= 48 && qk <= 57)
return String.fromCharCode(qk);
if (qk >= 16777264 && qk <= 16777298)
return "F" + (qk - 16777264 + 1);
return KEY_MAP[qk] || "";
}
function modsFromEvent(mods) {
var result = [];
var hasAlt = mods & 0x08000000;
var hasSuper = mods & 0x10000000;
if (hasAlt && hasSuper) {
result.push("Mod");
} else {
if (hasSuper)
result.push("Super");
if (hasAlt)
result.push("Alt");
}
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)
result.push("Shift");
return result;
}
function formatToken(mods, key) {
return (mods.length ? mods.join("+") + "+" : "") + key;
}
function normalizeKeyCombo(keyCombo) {
if (!keyCombo)
return "";
return keyCombo.toLowerCase().replace(/\bmod\b/g, "super").replace(/\bsuper\b/g, "super");
}
function getConflictingBinds(keyCombo, currentAction, allBinds) {
if (!keyCombo)
return [];
var conflicts = [];
var normalizedKey = normalizeKeyCombo(keyCombo);
for (var i = 0; i < allBinds.length; i++) {
var bind = allBinds[i];
if (bind.action === currentAction)
continue;
for (var k = 0; k < bind.keys.length; k++) {
if (normalizeKeyCombo(bind.keys[k].key) === normalizedKey) {
conflicts.push({
action: bind.action,
desc: bind.desc || bind.action
});
break;
}
}
}
return conflicts;
}

View File

@@ -0,0 +1,495 @@
.pragma library
const ACTION_TYPES = [
{ id: "dms", label: "DMS Action", icon: "widgets" },
{ id: "compositor", label: "Compositor", icon: "desktop_windows" },
{ id: "spawn", label: "Run Command", icon: "terminal" },
{ id: "shell", label: "Shell Command", icon: "code" }
];
const DMS_ACTIONS = [
{ id: "spawn dms ipc call spotlight toggle", label: "App Launcher: Toggle" },
{ id: "spawn dms ipc call spotlight open", label: "App Launcher: Open" },
{ id: "spawn dms ipc call spotlight close", label: "App Launcher: Close" },
{ id: "spawn dms ipc call clipboard toggle", label: "Clipboard: Toggle" },
{ id: "spawn dms ipc call clipboard open", label: "Clipboard: Open" },
{ id: "spawn dms ipc call clipboard close", label: "Clipboard: Close" },
{ id: "spawn dms ipc call notifications toggle", label: "Notifications: Toggle" },
{ id: "spawn dms ipc call notifications open", label: "Notifications: Open" },
{ id: "spawn dms ipc call notifications close", label: "Notifications: Close" },
{ id: "spawn dms ipc call processlist toggle", label: "Task Manager: Toggle" },
{ id: "spawn dms ipc call processlist open", label: "Task Manager: Open" },
{ id: "spawn dms ipc call processlist close", label: "Task Manager: Close" },
{ id: "spawn dms ipc call processlist focusOrToggle", label: "Task Manager: Focus or Toggle" },
{ id: "spawn dms ipc call settings toggle", label: "Settings: Toggle" },
{ id: "spawn dms ipc call settings open", label: "Settings: Open" },
{ id: "spawn dms ipc call settings close", label: "Settings: Close" },
{ id: "spawn dms ipc call settings focusOrToggle", label: "Settings: Focus or Toggle" },
{ id: "spawn dms ipc call powermenu toggle", label: "Power Menu: Toggle" },
{ id: "spawn dms ipc call powermenu open", label: "Power Menu: Open" },
{ id: "spawn dms ipc call powermenu close", label: "Power Menu: Close" },
{ id: "spawn dms ipc call control-center toggle", label: "Control Center: Toggle" },
{ id: "spawn dms ipc call control-center open", label: "Control Center: Open" },
{ id: "spawn dms ipc call control-center close", label: "Control Center: Close" },
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
{ id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
{ id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" },
{ id: "spawn dms ipc call dankdash wallpaper", label: "Wallpaper Browser" },
{ id: "spawn dms ipc call file browse wallpaper", label: "File: Browse Wallpaper" },
{ id: "spawn dms ipc call file browse profile", label: "File: Browse Profile" },
{ id: "spawn dms ipc call keybinds toggle niri", label: "Keybinds Cheatsheet: Toggle", compositor: "niri" },
{ id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" },
{ id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" },
{ id: "spawn dms ipc call lock lock", label: "Lock Screen" },
{ id: "spawn dms ipc call lock demo", label: "Lock Screen: Demo" },
{ id: "spawn dms ipc call inhibit toggle", label: "Idle Inhibit: Toggle" },
{ id: "spawn dms ipc call inhibit enable", label: "Idle Inhibit: Enable" },
{ id: "spawn dms ipc call inhibit disable", label: "Idle Inhibit: Disable" },
{ id: "spawn dms ipc call audio increment", label: "Volume Up" },
{ id: "spawn dms ipc call audio increment 1", label: "Volume Up (1%)" },
{ id: "spawn dms ipc call audio increment 5", label: "Volume Up (5%)" },
{ id: "spawn dms ipc call audio increment 10", label: "Volume Up (10%)" },
{ id: "spawn dms ipc call audio decrement", label: "Volume Down" },
{ id: "spawn dms ipc call audio decrement 1", label: "Volume Down (1%)" },
{ id: "spawn dms ipc call audio decrement 5", label: "Volume Down (5%)" },
{ id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" },
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" },
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
{ id: "spawn dms ipc call brightness increment", label: "Brightness Up" },
{ id: "spawn dms ipc call brightness increment 1", label: "Brightness Up (1%)" },
{ id: "spawn dms ipc call brightness increment 5", label: "Brightness Up (5%)" },
{ id: "spawn dms ipc call brightness increment 10", label: "Brightness Up (10%)" },
{ id: "spawn dms ipc call brightness decrement", label: "Brightness Down" },
{ id: "spawn dms ipc call brightness decrement 1", label: "Brightness Down (1%)" },
{ id: "spawn dms ipc call brightness decrement 5", label: "Brightness Down (5%)" },
{ id: "spawn dms ipc call brightness decrement 10", label: "Brightness Down (10%)" },
{ id: "spawn dms ipc call brightness toggleExponential", label: "Brightness: Toggle Exponential" },
{ id: "spawn dms ipc call theme toggle", label: "Theme: Toggle Light/Dark" },
{ id: "spawn dms ipc call theme light", label: "Theme: Light Mode" },
{ id: "spawn dms ipc call theme dark", label: "Theme: Dark Mode" },
{ id: "spawn dms ipc call night toggle", label: "Night Mode: Toggle" },
{ id: "spawn dms ipc call night enable", label: "Night Mode: Enable" },
{ id: "spawn dms ipc call night disable", label: "Night Mode: Disable" },
{ id: "spawn dms ipc call bar toggle index 0", label: "Bar: Toggle (Primary)" },
{ id: "spawn dms ipc call bar reveal index 0", label: "Bar: Reveal (Primary)" },
{ id: "spawn dms ipc call bar hide index 0", label: "Bar: Hide (Primary)" },
{ id: "spawn dms ipc call bar toggleAutoHide index 0", label: "Bar: Toggle Auto-Hide (Primary)" },
{ id: "spawn dms ipc call bar autoHide index 0", label: "Bar: Enable Auto-Hide (Primary)" },
{ id: "spawn dms ipc call bar manualHide index 0", label: "Bar: Disable Auto-Hide (Primary)" },
{ id: "spawn dms ipc call dock toggle", label: "Dock: Toggle" },
{ id: "spawn dms ipc call dock reveal", label: "Dock: Reveal" },
{ id: "spawn dms ipc call dock hide", label: "Dock: Hide" },
{ id: "spawn dms ipc call dock toggleAutoHide", label: "Dock: Toggle Auto-Hide" },
{ id: "spawn dms ipc call dock autoHide", label: "Dock: Enable Auto-Hide" },
{ id: "spawn dms ipc call dock manualHide", label: "Dock: Disable Auto-Hide" },
{ id: "spawn dms ipc call mpris playPause", label: "Media: Play/Pause" },
{ id: "spawn dms ipc call mpris play", label: "Media: Play" },
{ id: "spawn dms ipc call mpris pause", label: "Media: Pause" },
{ id: "spawn dms ipc call mpris previous", label: "Media: Previous Track" },
{ id: "spawn dms ipc call mpris next", label: "Media: Next Track" },
{ id: "spawn dms ipc call mpris stop", label: "Media: Stop" },
{ id: "spawn dms ipc call niri screenshot", label: "Screenshot: Interactive", compositor: "niri" },
{ id: "spawn dms ipc call niri screenshotScreen", label: "Screenshot: Full Screen", compositor: "niri" },
{ id: "spawn dms ipc call niri screenshotWindow", label: "Screenshot: Window", compositor: "niri" },
{ id: "spawn dms ipc call hypr toggleOverview", label: "Hyprland: Toggle Overview", compositor: "hyprland" },
{ id: "spawn dms ipc call hypr openOverview", label: "Hyprland: Open Overview", compositor: "hyprland" },
{ id: "spawn dms ipc call hypr closeOverview", label: "Hyprland: Close Overview", compositor: "hyprland" },
{ id: "spawn dms ipc call wallpaper next", label: "Wallpaper: Next" },
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }
];
const COMPOSITOR_ACTIONS = {
"Window": [
{ id: "close-window", label: "Close Window" },
{ id: "fullscreen-window", label: "Fullscreen" },
{ id: "maximize-column", label: "Maximize Column" },
{ id: "center-column", label: "Center Column" },
{ id: "center-visible-columns", label: "Center Visible Columns" },
{ id: "toggle-window-floating", label: "Toggle Floating" },
{ id: "switch-focus-between-floating-and-tiling", label: "Switch Floating/Tiling Focus" },
{ id: "switch-preset-column-width", label: "Cycle Column Width" },
{ id: "switch-preset-window-height", label: "Cycle Window Height" },
{ id: "set-column-width", label: "Set Column Width" },
{ id: "set-window-height", label: "Set Window Height" },
{ id: "reset-window-height", label: "Reset Window Height" },
{ id: "expand-column-to-available-width", label: "Expand to Available Width" },
{ id: "consume-or-expel-window-left", label: "Consume/Expel Left" },
{ id: "consume-or-expel-window-right", label: "Consume/Expel Right" },
{ id: "toggle-column-tabbed-display", label: "Toggle Tabbed" }
],
"Focus": [
{ id: "focus-column-left", label: "Focus Left" },
{ id: "focus-column-right", label: "Focus Right" },
{ id: "focus-window-down", label: "Focus Down" },
{ id: "focus-window-up", label: "Focus Up" },
{ id: "focus-column-first", label: "Focus First Column" },
{ id: "focus-column-last", label: "Focus Last Column" }
],
"Move": [
{ id: "move-column-left", label: "Move Left" },
{ id: "move-column-right", label: "Move Right" },
{ id: "move-window-down", label: "Move Down" },
{ id: "move-window-up", label: "Move Up" },
{ id: "move-column-to-first", label: "Move to First" },
{ id: "move-column-to-last", label: "Move to Last" }
],
"Workspace": [
{ id: "focus-workspace-down", label: "Focus Workspace Down" },
{ id: "focus-workspace-up", label: "Focus Workspace Up" },
{ id: "focus-workspace-previous", label: "Focus Previous Workspace" },
{ id: "focus-workspace", label: "Focus Workspace (by index)" },
{ id: "move-column-to-workspace-down", label: "Move to Workspace Down" },
{ id: "move-column-to-workspace-up", label: "Move to Workspace Up" },
{ id: "move-column-to-workspace", label: "Move to Workspace (by index)" },
{ id: "move-workspace-down", label: "Move Workspace Down" },
{ id: "move-workspace-up", label: "Move Workspace Up" }
],
"Monitor": [
{ id: "focus-monitor-left", label: "Focus Monitor Left" },
{ id: "focus-monitor-right", label: "Focus Monitor Right" },
{ id: "focus-monitor-down", label: "Focus Monitor Down" },
{ id: "focus-monitor-up", label: "Focus Monitor Up" },
{ id: "move-column-to-monitor-left", label: "Move to Monitor Left" },
{ id: "move-column-to-monitor-right", label: "Move to Monitor Right" },
{ id: "move-column-to-monitor-down", label: "Move to Monitor Down" },
{ id: "move-column-to-monitor-up", label: "Move to Monitor Up" }
],
"Screenshot": [
{ id: "screenshot", label: "Screenshot (Interactive)" },
{ id: "screenshot-screen", label: "Screenshot Screen" },
{ id: "screenshot-window", label: "Screenshot Window" }
],
"System": [
{ id: "toggle-overview", label: "Toggle Overview" },
{ id: "show-hotkey-overlay", label: "Show Hotkey Overlay" },
{ id: "power-off-monitors", label: "Power Off Monitors" },
{ id: "power-on-monitors", label: "Power On Monitors" },
{ id: "toggle-keyboard-shortcuts-inhibit", label: "Toggle Shortcuts Inhibit" },
{ id: "quit", label: "Quit Niri" },
{ id: "suspend", label: "Suspend" }
],
"Alt-Tab": [
{ id: "next-window", label: "Next Window" },
{ id: "previous-window", label: "Previous Window" }
]
};
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"];
const ACTION_ARGS = {
"set-column-width": {
args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }]
},
"set-window-height": {
args: [{ name: "value", type: "text", label: "Height", placeholder: "+10%, -10%, 50%" }]
},
"focus-workspace": {
args: [{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." }]
},
"move-column-to-workspace": {
args: [
{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." },
{ name: "focus", type: "bool", label: "Follow focus", default: false }
]
},
"screenshot": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
},
"screenshot-screen": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
},
"screenshot-window": {
args: [{ name: "opts", type: "screenshot", label: "Options" }]
}
};
const DMS_ACTION_ARGS = {
"audio increment": {
base: "spawn dms ipc call audio increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"audio decrement": {
base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness increment": {
base: "spawn dms ipc call brightness increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness decrement": {
base: "spawn dms ipc call brightness decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
}
};
function getActionTypes() {
return ACTION_TYPES;
}
function getDmsActions(isNiri, isHyprland) {
const result = [];
for (let i = 0; i < DMS_ACTIONS.length; i++) {
const action = DMS_ACTIONS[i];
if (!action.compositor) {
result.push(action);
continue;
}
switch (action.compositor) {
case "niri":
if (isNiri)
result.push(action);
break;
case "hyprland":
if (isHyprland)
result.push(action);
break;
}
}
return result;
}
function getCompositorCategories() {
return Object.keys(COMPOSITOR_ACTIONS);
}
function getCompositorActions(category) {
return COMPOSITOR_ACTIONS[category] || [];
}
function getCategoryOrder() {
return CATEGORY_ORDER;
}
function findDmsAction(actionId) {
for (let i = 0; i < DMS_ACTIONS.length; i++) {
if (DMS_ACTIONS[i].id === actionId)
return DMS_ACTIONS[i];
}
return null;
}
function findCompositorAction(actionId) {
for (const cat in COMPOSITOR_ACTIONS) {
const acts = COMPOSITOR_ACTIONS[cat];
for (let i = 0; i < acts.length; i++) {
if (acts[i].id === actionId)
return acts[i];
}
}
return null;
}
function getActionLabel(action) {
if (!action)
return "";
const dmsAct = findDmsAction(action);
if (dmsAct)
return dmsAct.label;
const compAct = findCompositorAction(action);
if (compAct)
return compAct.label;
if (action.startsWith("spawn sh -c "))
return action.slice(12).replace(/^["']|["']$/g, "");
if (action.startsWith("spawn "))
return action.slice(6);
return action;
}
function getActionType(action) {
if (!action)
return "compositor";
if (action.startsWith("spawn dms ipc call "))
return "dms";
if (action.startsWith("spawn sh -c ") || action.startsWith("spawn bash -c "))
return "shell";
if (action.startsWith("spawn "))
return "spawn";
return "compositor";
}
function isDmsAction(action) {
if (!action)
return false;
return action.startsWith("spawn dms ipc call ");
}
function isValidAction(action) {
if (!action)
return false;
switch (action) {
case "spawn":
case "spawn ":
case "spawn sh -c \"\"":
case "spawn sh -c ''":
return false;
}
return true;
}
function isKnownCompositorAction(action) {
if (!action)
return false;
return findCompositorAction(action) !== null;
}
function buildSpawnAction(command, args) {
if (!command)
return "";
let parts = [command];
if (args && args.length > 0)
parts = parts.concat(args.filter(function(a) { return a; }));
return "spawn " + parts.join(" ");
}
function buildShellAction(shellCmd) {
if (!shellCmd)
return "";
return "spawn sh -c \"" + shellCmd.replace(/"/g, "\\\"") + "\"";
}
function parseSpawnCommand(action) {
if (!action || !action.startsWith("spawn "))
return { command: "", args: [] };
const rest = action.slice(6);
const parts = rest.split(" ").filter(function(p) { return p; });
return {
command: parts[0] || "",
args: parts.slice(1)
};
}
function parseShellCommand(action) {
if (!action)
return "";
if (!action.startsWith("spawn sh -c "))
return "";
var content = action.slice(12);
if ((content.startsWith('"') && content.endsWith('"')) || (content.startsWith("'") && content.endsWith("'")))
content = content.slice(1, -1);
return content.replace(/\\"/g, "\"");
}
function getActionArgConfig(action) {
if (!action)
return null;
var baseAction = action.split(" ")[0];
if (ACTION_ARGS[baseAction])
return { type: "compositor", base: baseAction, config: ACTION_ARGS[baseAction] };
for (var key in DMS_ACTION_ARGS) {
if (action.startsWith(DMS_ACTION_ARGS[key].base))
return { type: "dms", base: key, config: DMS_ACTION_ARGS[key] };
}
return null;
}
function parseCompositorActionArgs(action) {
if (!action)
return { base: "", args: {} };
var parts = action.split(" ");
var base = parts[0];
var args = {};
if (!ACTION_ARGS[base])
return { base: action, args: {} };
var argConfig = ACTION_ARGS[base];
var argParts = parts.slice(1);
if (base === "move-column-to-workspace") {
for (var i = 0; i < argParts.length; i++) {
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
args.focus = argParts[i] === "focus=true";
} else if (!args.index) {
args.index = argParts[i];
}
}
} else if (base.startsWith("screenshot")) {
args.opts = {};
for (var j = 0; j < argParts.length; j += 2) {
if (j + 1 < argParts.length)
args.opts[argParts[j]] = argParts[j + 1];
}
} else if (argParts.length > 0) {
args.value = argParts.join(" ");
}
return { base: base, args: args };
}
function buildCompositorAction(base, args) {
if (!base)
return "";
var parts = [base];
if (!args || Object.keys(args).length === 0)
return base;
if (base === "move-column-to-workspace") {
if (args.index)
parts.push(args.index);
if (args.focus === true)
parts.push("focus=true");
else if (args.focus === false)
parts.push("focus=false");
} else if (base.startsWith("screenshot") && args.opts) {
for (var key in args.opts) {
if (args.opts[key] !== undefined && args.opts[key] !== "") {
parts.push(key);
parts.push(args.opts[key]);
}
}
} else if (args.value) {
parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
}
return parts.join(" ");
}
function parseDmsActionArgs(action) {
if (!action)
return { base: "", args: {} };
for (var key in DMS_ACTION_ARGS) {
var config = DMS_ACTION_ARGS[key];
if (action.startsWith(config.base)) {
var rest = action.slice(config.base.length).trim();
return { base: key, args: { amount: rest || "" } };
}
}
return { base: action, args: {} };
}
function buildDmsAction(baseKey, args) {
var config = DMS_ACTION_ARGS[baseKey];
if (!config)
return "";
var action = config.base;
if (args && args.amount)
action += " " + args.amount;
return action;
}
function getScreenshotOptions() {
return [
{ id: "write-to-disk", label: "Save to disk", type: "bool" },
{ id: "show-pointer", label: "Show pointer", type: "bool" }
];
}

View File

@@ -16,7 +16,15 @@ Singleton {
const currentOSD = currentOSDsByScreen[screenName];
if (currentOSD && currentOSD !== osd) {
currentOSD.hide();
if (typeof currentOSD.hide === "function") {
try {
currentOSD.hide();
} catch (e) {
currentOSDsByScreen[screenName] = null;
}
} else {
currentOSDsByScreen[screenName] = null;
}
}
currentOSDsByScreen[screenName] = osd;

View File

@@ -3,122 +3,139 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
readonly property int noTimeout: -1
property int defaultDebounceMs: 50
property int defaultTimeoutMs: 10000
property var _procDebouncers: ({})
function runCommand(id, command, callback, debounceMs, timeoutMs) {
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
const timeout = (typeof timeoutMs === "number" && timeoutMs > 0) ? timeoutMs : defaultTimeoutMs
let procId = id ? id : Math.random()
const isRandomId = !id
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs;
const timeout = (typeof timeoutMs === "number") ? timeoutMs : defaultTimeoutMs;
let procId = id ? id : Math.random();
const isRandomId = !id;
if (!_procDebouncers[procId]) {
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
t.triggered.connect(function() { _launchProc(procId, isRandomId) })
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait, timeoutMs: timeout, isRandomId: isRandomId }
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root);
t.triggered.connect(function () {
_launchProc(procId, isRandomId);
});
_procDebouncers[procId] = {
timer: t,
command: command,
callback: callback,
waitMs: wait,
timeoutMs: timeout,
isRandomId: isRandomId
};
} else {
_procDebouncers[procId].command = command
_procDebouncers[procId].callback = callback
_procDebouncers[procId].waitMs = wait
_procDebouncers[procId].timeoutMs = timeout
_procDebouncers[procId].command = command;
_procDebouncers[procId].callback = callback;
_procDebouncers[procId].waitMs = wait;
_procDebouncers[procId].timeoutMs = timeout;
}
const entry = _procDebouncers[procId]
entry.timer.interval = entry.waitMs
entry.timer.restart()
const entry = _procDebouncers[procId];
entry.timer.interval = entry.waitMs;
entry.timer.restart();
}
function _launchProc(id, isRandomId) {
const entry = _procDebouncers[id]
if (!entry) return
const entry = _procDebouncers[id];
if (!entry)
return;
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root);
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc);
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc);
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root);
const proc = Qt.createQmlObject('import Quickshell.Io; Process { running: false }', root)
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
proc.stdout = out;
proc.stderr = err;
proc.command = entry.command;
proc.stdout = out
proc.stderr = err
proc.command = entry.command
let capturedOut = "";
let capturedErr = "";
let exitSeen = false;
let exitCodeValue = -1;
let outSeen = false;
let errSeen = false;
let timedOut = false;
let capturedOut = ""
let capturedErr = ""
let exitSeen = false
let exitCodeValue = -1
let outSeen = false
let errSeen = false
let timedOut = false
timeoutTimer.interval = entry.timeoutMs
timeoutTimer.triggered.connect(function() {
timeoutTimer.interval = entry.timeoutMs;
timeoutTimer.triggered.connect(function () {
if (!exitSeen) {
timedOut = true
proc.running = false
exitSeen = true
exitCodeValue = 124
maybeComplete()
timedOut = true;
proc.running = false;
exitSeen = true;
exitCodeValue = 124;
maybeComplete();
}
})
});
out.streamFinished.connect(function() {
out.streamFinished.connect(function () {
try {
capturedOut = out.text || ""
capturedOut = out.text || "";
} catch (e) {
capturedOut = ""
capturedOut = "";
}
outSeen = true
maybeComplete()
})
outSeen = true;
maybeComplete();
});
err.streamFinished.connect(function() {
err.streamFinished.connect(function () {
try {
capturedErr = err.text || ""
capturedErr = err.text || "";
} catch (e) {
capturedErr = ""
capturedErr = "";
}
errSeen = true
maybeComplete()
})
errSeen = true;
maybeComplete();
});
proc.exited.connect(function(code) {
timeoutTimer.stop()
exitSeen = true
exitCodeValue = code
maybeComplete()
})
proc.exited.connect(function (code) {
timeoutTimer.stop();
exitSeen = true;
exitCodeValue = code;
maybeComplete();
});
function maybeComplete() {
if (!exitSeen || !outSeen || !errSeen) return
timeoutTimer.stop()
if (!exitSeen || !outSeen || !errSeen)
return;
timeoutTimer.stop();
if (entry && entry.callback && typeof entry.callback === "function") {
try {
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : ""
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1
entry.callback(safeOutput, safeExitCode)
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : "";
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1;
entry.callback(safeOutput, safeExitCode);
} catch (e) {
console.warn("runCommand callback error for command:", entry.command, "Error:", e)
console.warn("runCommand callback error for command:", entry.command, "Error:", e);
}
}
try { proc.destroy() } catch (_) {}
try { timeoutTimer.destroy() } catch (_) {}
try {
proc.destroy();
} catch (_) {}
try {
timeoutTimer.destroy();
} catch (_) {}
if (isRandomId || entry.isRandomId) {
Qt.callLater(function() {
Qt.callLater(function () {
if (_procDebouncers[id]) {
try { _procDebouncers[id].timer.destroy() } catch (_) {}
delete _procDebouncers[id]
try {
_procDebouncers[id].timer.destroy();
} catch (_) {}
delete _procDebouncers[id];
}
})
});
}
}
proc.running = true
timeoutTimer.start()
proc.running = true;
if (entry.timeoutMs !== noTimeout)
timeoutTimer.start();
}
}

View File

@@ -105,7 +105,7 @@ Singleton {
property bool controlCenterShowNetworkIcon: true
property bool controlCenterShowBluetoothIcon: true
property bool controlCenterShowAudioIcon: true
property bool controlCenterShowVpnIcon: false
property bool controlCenterShowVpnIcon: true
property bool controlCenterShowBrightnessIcon: false
property bool controlCenterShowMicIcon: false
property bool controlCenterShowBatteryIcon: false
@@ -168,12 +168,14 @@ Singleton {
property bool dwlShowAllTags: false
property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true
property bool scrollTitleEnabled: true
property bool clockCompactMode: false
property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true
property bool keyboardLayoutNameCompactMode: false
property bool runningAppsCurrentWorkspace: false
property bool runningAppsGroupByApp: false
property string centeringMode: "index"
property string clockDateFormat: ""
property string lockDateFormat: ""
property int mediaSize: 1
@@ -185,6 +187,7 @@ Singleton {
property bool sortAppsAlphabetically: false
property int appLauncherGridColumns: 4
property bool spotlightCloseNiriOverview: true
property bool niriOverviewOverlayEnabled: true
property string weatherLocation: "New York, NY"
property string weatherCoordinates: "40.7128,-74.0060"
@@ -226,6 +229,7 @@ Singleton {
onNotepadFontFamilyChanged: saveSettings()
onNotepadFontSizeChanged: saveSettings()
onNotepadShowLineNumbersChanged: saveSettings()
// onCenteringModeChanged: saveSettings()
onNotepadTransparencyOverrideChanged: {
if (notepadTransparencyOverride > 0) {
notepadLastCustomTransparency = notepadTransparencyOverride;
@@ -244,10 +248,12 @@ Singleton {
property int acLockTimeout: 0
property int acSuspendTimeout: 0
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
property string acProfileName: ""
property int batteryMonitorTimeout: 0
property int batteryLockTimeout: 0
property int batterySuspendTimeout: 0
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
property string batteryProfileName: ""
property bool lockBeforeSuspend: false
property bool preventIdleForMedia: false
property bool loginctlLockIntegration: true
@@ -291,6 +297,8 @@ Singleton {
property bool enableFprint: false
property int maxFprintTries: 3
property bool fprintdAvailable: false
property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000"
property bool hideBrightnessSlider: false
property int notificationTimeoutLow: 5000
@@ -307,9 +315,10 @@ Singleton {
property bool osdMicMuteEnabled: true
property bool osdCapsLockEnabled: true
property bool osdPowerProfileEnabled: true
property bool osdAudioOutputEnabled: true
property bool powerActionConfirm: true
property int powerActionHoldDuration: 1
property real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
property string powerMenuDefaultAction: "logout"
property bool powerMenuGridLayout: false
@@ -363,7 +372,8 @@ Singleton {
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4
popupGapsManual: 4,
maximizeDetection: true
}
]

View File

@@ -51,7 +51,7 @@ var SPEC = {
controlCenterShowNetworkIcon: { def: true },
controlCenterShowBluetoothIcon: { def: true },
controlCenterShowAudioIcon: { def: true },
controlCenterShowVpnIcon: { def: false },
controlCenterShowVpnIcon: { def: true },
controlCenterShowBrightnessIcon: { def: false },
controlCenterShowMicIcon: { def: false },
controlCenterShowBatteryIcon: { def: false },
@@ -83,12 +83,14 @@ var SPEC = {
dwlShowAllTags: { def: false },
workspaceNameIcons: { def: {} },
waveProgressEnabled: { def: true },
scrollTitleEnabled: {def: true},
clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false },
runningAppsCompactMode: { def: true },
keyboardLayoutNameCompactMode: { def: false },
runningAppsCurrentWorkspace: { def: false },
runningAppsGroupByApp: { def: false },
centeringMode: { def: "index" },
clockDateFormat: { def: "" },
lockDateFormat: { def: "" },
mediaSize: { def: 1 },
@@ -98,6 +100,7 @@ var SPEC = {
sortAppsAlphabetically: { def: false },
appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true },
niriOverviewOverlayEnabled: { def: true },
weatherLocation: { def: "New York, NY" },
weatherCoordinates: { def: "40.7128,-74.0060" },
@@ -144,10 +147,12 @@ var SPEC = {
acLockTimeout: { def: 0 },
acSuspendTimeout: { def: 0 },
acSuspendBehavior: { def: 0 },
acProfileName: { def: "" },
batteryMonitorTimeout: { def: 0 },
batteryLockTimeout: { def: 0 },
batterySuspendTimeout: { def: 0 },
batterySuspendBehavior: { def: 0 },
batteryProfileName: { def: "" },
lockBeforeSuspend: { def: false },
preventIdleForMedia: { def: false },
loginctlLockIntegration: { def: true },
@@ -191,6 +196,8 @@ var SPEC = {
enableFprint: { def: false },
maxFprintTries: { def: 3 },
fprintdAvailable: { def: false, persist: false },
lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" },
hideBrightnessSlider: { def: false },
notificationTimeoutLow: { def: 5000 },
@@ -207,9 +214,10 @@ var SPEC = {
osdMicMuteEnabled: { def: true },
osdCapsLockEnabled: { def: true },
osdPowerProfileEnabled: { def: false },
osdAudioOutputEnabled: { def: true },
powerActionConfirm: { def: true },
powerActionHoldDuration: { def: 1 },
powerActionHoldDuration: { def: 0.5 },
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
powerMenuDefaultAction: { def: "logout" },
powerMenuGridLayout: { def: false },
@@ -262,7 +270,8 @@ var SPEC = {
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4
popupGapsManual: 4,
maximizeDetection: true
}], onChange: "updateBarConfigs" }
};

View File

@@ -25,8 +25,6 @@ import qs.Services
Item {
id: root
readonly property bool _forceDisplayService: DisplayService.brightnessAvailable !== undefined
Instantiator {
id: daemonPluginInstantiator
asynchronous: true
@@ -702,6 +700,14 @@ Item {
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: AudioOutputOSD {
modelData: item
}
}
LazyLoader {
id: hyprlandOverviewLoader
active: CompositorService.isHyprland
@@ -712,7 +718,7 @@ Item {
LazyLoader {
id: niriOverviewOverlayLoader
active: CompositorService.isNiri
active: CompositorService.isNiri && SettingsData.niriOverviewOverlayEnabled
component: NiriOverviewOverlay {
id: niriOverviewOverlay
}

View File

@@ -329,79 +329,79 @@ Item {
IpcHandler {
function toggle(provider: string): string {
if (!provider) {
if (!provider)
return "ERROR: No provider specified";
}
KeybindsService.loadProvider(provider);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
} else {
root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
} else {
root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
}
function toggleWithPath(provider: string, path: string): string {
if (!provider) {
if (!provider)
return "ERROR: No provider specified";
}
KeybindsService.loadProviderWithPath(provider, path);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
} else {
root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
} else {
root.hyprKeybindsModalLoader.item.open();
}
return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
}
function open(provider: string): string {
if (!provider) {
if (!provider)
return "ERROR: No provider specified";
}
KeybindsService.loadProvider(provider);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider}`;
}
return `KEYBINDS_OPEN_FAILED: ${provider}`;
if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_OPEN_FAILED: ${provider}`;
root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider}`;
}
function openWithPath(provider: string, path: string): string {
if (!provider) {
if (!provider)
return "ERROR: No provider specified";
}
KeybindsService.loadProviderWithPath(provider, path);
KeybindsService.currentProvider = provider;
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})`;
}
return `KEYBINDS_OPEN_FAILED: ${provider}`;
if (!root.hyprKeybindsModalLoader.item)
return `KEYBINDS_OPEN_FAILED: ${provider}`;
root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})`;
}
function close(): string {
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close();
return "KEYBINDS_CLOSE_SUCCESS";
}
return "KEYBINDS_CLOSE_FAILED";
if (!root.hyprKeybindsModalLoader.item)
return "KEYBINDS_CLOSE_FAILED";
root.hyprKeybindsModalLoader.item.close();
return "KEYBINDS_CLOSE_SUCCESS";
}
target: "keybinds"
@@ -409,44 +409,48 @@ Item {
IpcHandler {
function openBinds(): string {
if (!CompositorService.isHyprland) {
if (!CompositorService.isHyprland)
return "HYPR_NOT_AVAILABLE";
}
KeybindsService.loadProvider("hyprland");
KeybindsService.currentProvider = "hyprland";
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open();
return "HYPR_KEYBINDS_OPEN_SUCCESS";
}
return "HYPR_KEYBINDS_OPEN_FAILED";
if (!root.hyprKeybindsModalLoader.item)
return "HYPR_KEYBINDS_OPEN_FAILED";
root.hyprKeybindsModalLoader.item.open();
return "HYPR_KEYBINDS_OPEN_SUCCESS";
}
function closeBinds(): string {
if (!CompositorService.isHyprland) {
if (!CompositorService.isHyprland)
return "HYPR_NOT_AVAILABLE";
}
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close();
return "HYPR_KEYBINDS_CLOSE_SUCCESS";
}
return "HYPR_KEYBINDS_CLOSE_FAILED";
if (!root.hyprKeybindsModalLoader.item)
return "HYPR_KEYBINDS_CLOSE_FAILED";
root.hyprKeybindsModalLoader.item.close();
return "HYPR_KEYBINDS_CLOSE_SUCCESS";
}
function toggleBinds(): string {
if (!CompositorService.isHyprland) {
if (!CompositorService.isHyprland)
return "HYPR_NOT_AVAILABLE";
}
KeybindsService.loadProvider("hyprland");
KeybindsService.currentProvider = "hyprland";
KeybindsService.loadBinds();
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
} else {
root.hyprKeybindsModalLoader.item.open();
}
return "HYPR_KEYBINDS_TOGGLE_SUCCESS";
if (!root.hyprKeybindsModalLoader.item)
return "HYPR_KEYBINDS_TOGGLE_FAILED";
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close();
} else {
root.hyprKeybindsModalLoader.item.open();
}
return "HYPR_KEYBINDS_TOGGLE_FAILED";
return "HYPR_KEYBINDS_TOGGLE_SUCCESS";
}
function toggleOverview(): string {
@@ -490,60 +494,108 @@ Item {
function getBarConfig(selector: string, value: string): var {
const barSelectors = ["id", "name", "index"];
if (!barSelectors.includes(selector)) return { error: "BAR_INVALID_SELECTOR" };
if (!barSelectors.includes(selector))
return {
error: "BAR_INVALID_SELECTOR"
};
const index = selector === "index" ? Number(value) : SettingsData.barConfigs.findIndex(bar => bar[selector] == value);
const barConfig = SettingsData.barConfigs?.[index];
if (!barConfig) return { error: "BAR_NOT_FOUND" };
return { barConfig };
if (!barConfig)
return {
error: "BAR_NOT_FOUND"
};
return {
barConfig
};
}
IpcHandler {
function reveal(selector: string, value: string): string {
const { barConfig, error } = getBarConfig(selector, value);
if (error) return error;
SettingsData.updateBarConfig(barConfig.id, {visible: true});
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
SettingsData.updateBarConfig(barConfig.id, {
visible: true
});
return "BAR_SHOW_SUCCESS";
}
function hide(selector: string, value: string): string {
const { barConfig, error } = getBarConfig(selector, value);
if (error) return error;
SettingsData.updateBarConfig(barConfig.id, {visible: false});
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
SettingsData.updateBarConfig(barConfig.id, {
visible: false
});
return "BAR_HIDE_SUCCESS";
}
function toggle(selector: string, value: string): string {
const { barConfig, error } = getBarConfig(selector, value);
if (error) return error;
SettingsData.updateBarConfig(barConfig.id, {visible: !barConfig.visible});
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
SettingsData.updateBarConfig(barConfig.id, {
visible: !barConfig.visible
});
return !barConfig.visible ? "BAR_SHOW_SUCCESS" : "BAR_HIDE_SUCCESS";
}
function status(selector: string, value: string): string {
const { barConfig, error } = getBarConfig(selector, value);
if (error) return error;
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
return barConfig.visible ? "visible" : "hidden";
}
function autoHide(selector: string, value: string): string {
const { barConfig, error } = getBarConfig(selector, value);
if (error) return error;
SettingsData.updateBarConfig(barConfig.id, {autoHide: true});
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
SettingsData.updateBarConfig(barConfig.id, {
autoHide: true
});
return "BAR_AUTO_HIDE_SUCCESS";
}
function manualHide(selector: string, value: string): string {
const { barConfig, error } = getBarConfig(selector, value);
if (error) return error;
SettingsData.updateBarConfig(barConfig.id, {autoHide: false});
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
SettingsData.updateBarConfig(barConfig.id, {
autoHide: false
});
return "BAR_MANUAL_HIDE_SUCCESS";
}
function toggleAutoHide(selector: string, value: string): string {
const { barConfig, error } = getBarConfig(selector, value);
if (error) return error;
SettingsData.updateBarConfig(barConfig.id, {autoHide: !barConfig.autoHide});
return barConfig.autoHide ? "BAR_MANUAL_HIDE_SUCCESS": "BAR_AUTO_HIDE_SUCCESS";
const {
barConfig,
error
} = getBarConfig(selector, value);
if (error)
return error;
SettingsData.updateBarConfig(barConfig.id, {
autoHide: !barConfig.autoHide
});
return barConfig.autoHide ? "BAR_MANUAL_HIDE_SUCCESS" : "BAR_AUTO_HIDE_SUCCESS";
}
target: "bar"
@@ -570,20 +622,20 @@ Item {
}
function autoHide(): string {
SettingsData.dockAutoHide = true
SettingsData.saveSettings()
SettingsData.dockAutoHide = true;
SettingsData.saveSettings();
return "BAR_AUTO_HIDE_SUCCESS";
}
function manualHide(): string {
SettingsData.dockAutoHide = false
SettingsData.saveSettings()
SettingsData.dockAutoHide = false;
SettingsData.saveSettings();
return "BAR_MANUAL_HIDE_SUCCESS";
}
function toggleAutoHide(): string {
SettingsData.dockAutoHide = !SettingsData.dockAutoHide
SettingsData.saveSettings()
SettingsData.dockAutoHide = !SettingsData.dockAutoHide;
SettingsData.saveSettings();
return SettingsData.dockAutoHide ? "BAR_AUTO_HIDE_SUCCESS" : "BAR_MANUAL_HIDE_SUCCESS";
}
@@ -596,6 +648,13 @@ Item {
return "SETTINGS_OPEN_SUCCESS";
}
function openWith(tab: string): string {
if (!tab)
return "SETTINGS_OPEN_FAILED: No tab specified";
PopoutService.openSettingsWithTab(tab);
return `SETTINGS_OPEN_SUCCESS: ${tab}`;
}
function close(): string {
PopoutService.closeSettings();
return "SETTINGS_CLOSE_SUCCESS";
@@ -606,55 +665,93 @@ Item {
return "SETTINGS_TOGGLE_SUCCESS";
}
function toggleWith(tab: string): string {
if (!tab)
return "SETTINGS_TOGGLE_FAILED: No tab specified";
PopoutService.toggleSettingsWithTab(tab);
return `SETTINGS_TOGGLE_SUCCESS: ${tab}`;
}
function focusOrToggle(): string {
PopoutService.focusOrToggleSettings();
return "SETTINGS_FOCUS_OR_TOGGLE_SUCCESS";
}
function focusOrToggleWith(tab: string): string {
if (!tab)
return "SETTINGS_FOCUS_OR_TOGGLE_FAILED: No tab specified";
PopoutService.focusOrToggleSettingsWithTab(tab);
return `SETTINGS_FOCUS_OR_TOGGLE_SUCCESS: ${tab}`;
}
function tabs(): string {
if (!PopoutService.settingsModal)
return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_widgets\nworkspaces\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout";
var modal = PopoutService.settingsModal;
var ids = [];
var structure = modal.sidebar?.categoryStructure ?? [];
for (var i = 0; i < structure.length; i++) {
var cat = structure[i];
if (cat.separator)
continue;
if (cat.id)
ids.push(cat.id);
if (cat.children) {
for (var j = 0; j < cat.children.length; j++) {
if (cat.children[j].id)
ids.push(cat.children[j].id);
}
}
}
return ids.join("\n");
}
function get(key: string): string {
return JSON.stringify(SettingsData?.[key])
return JSON.stringify(SettingsData?.[key]);
}
function set(key: string, value: string): string {
if (!(key in SettingsData)) {
console.warn("Cannot set property, not found:", key)
return "SETTINGS_INVALID_KEY"
console.warn("Cannot set property, not found:", key);
return "SETTINGS_INVALID_KEY";
}
const typeName = typeof SettingsData?.[key]
const typeName = typeof SettingsData?.[key];
try {
switch (typeName) {
case "boolean":
if (value === "true" || value === "false") value = (value === "true")
else throw `${value} is not a Boolean`
break
if (value === "true" || value === "false")
value = (value === "true");
else
throw `${value} is not a Boolean`;
break;
case "number":
value = Number(value)
if (isNaN(value)) throw `${value} is not a Number`
break
value = Number(value);
if (isNaN(value))
throw `${value} is not a Number`;
break;
case "string":
value = String(value)
break
value = String(value);
break;
case "object":
// NOTE: Parsing lists is messed up upstream and not sure if we want
// to make sure objects are well structured or just let people set
// whatever they want but risking messed up settings.
// Objects & Arrays are disabled for now
// https://github.com/quickshell-mirror/quickshell/pull/22
throw "Setting Objects and Arrays not supported"
throw "Setting Objects and Arrays not supported";
default:
throw "Unsupported type"
throw "Unsupported type";
}
console.warn("Setting:", key, value)
SettingsData[key] = value
SettingsData.saveSettings()
return "SETTINGS_SET_SUCCESS"
console.warn("Setting:", key, value);
SettingsData[key] = value;
SettingsData.saveSettings();
return "SETTINGS_SET_SUCCESS";
} catch (e) {
console.warn("Failed to set property:", key, "error:", e)
return "SETTINGS_SET_FAILURE"
console.warn("Failed to set property:", key, "error:", e);
return "SETTINGS_SET_FAILURE";
}
}

View File

@@ -27,10 +27,12 @@ DankModal {
modalWidth: 520
modalHeight: 500
onBackgroundClicked: close()
onDialogClosed: {
searchQuery = ""
selectedIndex = 0
keyboardNavigationActive: false
keyboardNavigationActive = false
}
onOpened: {

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -9,6 +10,11 @@ DankModal {
layerNamespace: "dms:bluetooth-pairing"
HyprlandFocusGrab {
windows: [root.contentWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}
property string deviceName: ""
property string deviceAddress: ""
property string requestType: ""

View File

@@ -1,9 +1,10 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modals.Clipboard
FocusScope {
Item {
id: clipboardContent
required property var modal
@@ -14,7 +15,6 @@ FocusScope {
property alias clipboardListView: clipboardListView
anchors.fill: parent
focus: true
Column {
anchors.fill: parent
@@ -31,13 +31,14 @@ FocusScope {
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
onClearAllClicked: {
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
modal.clearAll();
modal.hide();
}, function () {});
modal.clearAll()
modal.hide()
}, function () {})
}
onCloseClicked: modal.hide()
}
// Search Field
DankTextField {
id: searchField
width: parent.width
@@ -46,24 +47,27 @@ FocusScope {
showClearButton: true
focus: true
ignoreTabKeys: true
keyForwardTargets: [modal.modalFocusScope]
onTextChanged: {
modal.searchText = text;
modal.updateFilteredModel();
modal.searchText = text
modal.updateFilteredModel()
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
modal.hide();
event.accepted = true;
return;
}
modal.keyboardController?.handleKey(event);
Keys.onEscapePressed: function (event) {
modal.hide()
event.accepted = true
}
Component.onCompleted: {
Qt.callLater(function () {
forceActiveFocus()
})
}
Component.onCompleted: Qt.callLater(() => forceActiveFocus())
Connections {
target: modal
function onOpened() {
Qt.callLater(() => searchField.forceActiveFocus());
Qt.callLater(function () {
searchField.forceActiveFocus()
})
}
}
}
@@ -93,21 +97,21 @@ FocusScope {
function ensureVisible(index) {
if (index < 0 || index >= count) {
return;
return
}
const itemHeight = ClipboardConstants.itemHeight + spacing;
const itemY = index * itemHeight;
const itemBottom = itemY + itemHeight;
const itemHeight = ClipboardConstants.itemHeight + spacing
const itemY = index * itemHeight
const itemBottom = itemY + itemHeight
if (itemY < contentY) {
contentY = itemY;
contentY = itemY
} else if (itemBottom > contentY + height) {
contentY = itemBottom - height;
contentY = itemBottom - height
}
}
onCurrentIndexChanged: {
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
ensureVisible(currentIndex);
ensureVisible(currentIndex)
}
}

View File

@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
@@ -12,6 +13,11 @@ DankModal {
layerNamespace: "dms:clipboard"
HyprlandFocusGrab {
windows: [clipboardHistoryModal.contentWindow]
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
}
property int totalCount: 0
property var clipboardEntries: []
property string searchText: ""
@@ -142,10 +148,11 @@ DankModal {
borderWidth: 1
enableShadow: true
onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
content: clipboardContent
property alias keyboardController: keyboardController
ClipboardKeyboardController {
id: keyboardController
modal: clipboardHistoryModal
@@ -158,11 +165,13 @@ DankModal {
onVisibleChanged: {
if (visible) {
clipboardHistoryModal.shouldHaveFocus = false;
return;
} else if (clipboardHistoryModal.shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = true;
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
}
}
if (!clipboardHistoryModal.shouldBeVisible)
return;
clipboardHistoryModal.shouldHaveFocus = true;
}
}

View File

@@ -61,21 +61,29 @@ DankModal {
shouldBeVisible: false
allowStacking: true
modalWidth: 350
modalHeight: 160
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: {
close();
if (onCancel)
if (onCancel) {
onCancel();
}
}
function handleKey(event) {
onOpened: {
Qt.callLater(function () {
modalFocusScope.forceActiveFocus();
modalFocusScope.focus = true;
shouldHaveFocus = true;
});
}
modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) {
case Qt.Key_Escape:
close();
if (onCancel)
if (onCancel) {
onCancel();
}
event.accepted = true;
break;
case Qt.Key_Left:
@@ -91,46 +99,46 @@ DankModal {
event.accepted = true;
break;
case Qt.Key_N:
if (!(event.modifiers & Qt.ControlModifier))
return;
keyboardNavigation = true;
selectedButton = (selectedButton + 1) % 2;
event.accepted = true;
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true;
selectedButton = (selectedButton + 1) % 2;
event.accepted = true;
}
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier))
return;
keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
event.accepted = true;
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
event.accepted = true;
}
break;
case Qt.Key_J:
if (!(event.modifiers & Qt.ControlModifier))
return;
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
}
break;
case Qt.Key_K:
if (!(event.modifiers & Qt.ControlModifier))
return;
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
}
break;
case Qt.Key_H:
if (!(event.modifiers & Qt.ControlModifier))
return;
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
}
break;
case Qt.Key_L:
if (!(event.modifiers & Qt.ControlModifier))
return;
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
}
break;
case Qt.Key_Tab:
keyboardNavigation = true;
@@ -139,9 +147,9 @@ DankModal {
break;
case Qt.Key_Return:
case Qt.Key_Enter:
if (selectedButton !== -1)
if (selectedButton !== -1) {
selectButton();
else {
} else {
selectedButton = 1;
selectButton();
}
@@ -151,13 +159,10 @@ DankModal {
}
content: Component {
FocusScope {
Item {
anchors.fill: parent
focus: true
implicitHeight: mainColumn.implicitHeight
Keys.onPressed: event => root.handleKey(event)
Column {
id: mainColumn
anchors.left: parent.left

View File

@@ -1,19 +1,24 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property string layerNamespace: "dms:modal"
property Component content: null
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property Item directContent: null
property Item loadedContent: null
readonly property var contentLoader: QtObject {
readonly property var item: root.directContent ?? root.loadedContent
}
property real modalWidth: 400
property real modalHeight: 300
property var targetScreen: null
property var targetScreen
readonly property var effectiveScreen: contentWindow.screen ?? targetScreen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
property bool showBackground: true
property real backgroundOpacity: 0.5
property string positioning: "center"
@@ -31,6 +36,7 @@ Item {
property real borderWidth: 1
property real cornerRadius: Theme.cornerRadius
property bool enableShadow: false
property alias modalFocusScope: focusScope
property bool shouldBeVisible: false
property bool shouldHaveFocus: shouldBeVisible
property bool allowFocusOverride: false
@@ -39,41 +45,55 @@ Item {
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
property bool useOverlayLayer: false
readonly property alias contentWindow: contentWindow
readonly property alias backgroundWindow: backgroundWindow
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
signal opened
signal dialogClosed
signal backgroundClicked
onBackgroundClicked: {
if (closeOnBackgroundClick)
close();
}
property bool animationsEnabled: true
readonly property bool useBackgroundWindow: true
function open() {
ModalManager.openModal(root);
closeTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
contentWindow.screen = focusedScreen;
if (useBackgroundWindow)
backgroundWindow.screen = focusedScreen;
}
shouldBeVisible = true;
shouldHaveFocus = true;
DankModalWindow.showModal(root);
opened();
}
function openCentered() {
positioning = "center";
open();
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = true;
Qt.callLater(() => {
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => {
shouldHaveFocus = Qt.binding(() => shouldBeVisible);
});
});
}
function close() {
shouldBeVisible = false;
shouldHaveFocus = false;
DankModalWindow.hideModal();
dialogClosed();
closeTimer.restart();
}
function instantClose() {
animationsEnabled = false;
shouldBeVisible = false;
shouldHaveFocus = false;
DankModalWindow.hideModalInstant();
closeTimer.stop();
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
dialogClosed();
Qt.callLater(() => animationsEnabled = true);
}
function toggle() {
@@ -83,9 +103,346 @@ Item {
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal === root || allowStacking || !shouldBeVisible)
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
close();
}
}
}
Connections {
target: Quickshell
function onScreensChanged() {
if (!contentWindow.screen)
return;
close();
const currentScreenName = contentWindow.screen.name;
let screenStillExists = false;
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === currentScreenName) {
screenStillExists = true;
break;
}
}
if (screenStillExists)
return;
const newScreen = CompositorService.getFocusedScreen();
if (newScreen) {
contentWindow.screen = newScreen;
if (useBackgroundWindow)
backgroundWindow.screen = newScreen;
}
}
}
Timer {
id: closeTimer
interval: animationDuration + 120
onTriggered: {
if (!shouldBeVisible) {
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
dialogClosed();
}
}
}
readonly property real shadowBuffer: 5
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap((() => {
switch (positioning) {
case "center":
return (screenWidth - alignedWidth) / 2;
case "top-right":
return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
case "custom":
return customPosition.x;
default:
return 0;
}
})(), dpr)
readonly property real alignedY: Theme.snap((() => {
switch (positioning) {
case "center":
return (screenHeight - alignedHeight) / 2;
case "top-right":
return Theme.barHeight + Theme.spacingXS;
case "custom":
return customPosition.y;
default:
return 0;
}
})(), dpr)
PanelWindow {
id: backgroundWindow
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: Rectangle {
x: root.alignedX
y: root.alignedY
width: root.shouldBeVisible ? root.alignedWidth : 0
height: root.shouldBeVisible ? root.alignedHeight : 0
}
intersection: Intersection.Xor
}
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const clickX = mouse.x;
const clickY = mouse.y;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
if (!outsideContent)
return;
root.backgroundClicked();
}
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
if (root.useOverlayLayer)
return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
return customKeyboardFocus;
if (!shouldHaveFocus)
return WlrKeyboardFocus.None;
if (root.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
}
implicitWidth: root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible) {
opened();
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
}
}
Item {
id: modalContainer
x: shadowBuffer
y: shadowBuffer
width: root.alignedWidth
height: root.alignedHeight
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
}
}
Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
Item {
id: animatedContent
anchors.fill: parent
clip: false
opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
radius: root.cornerRadius
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
Item {
id: directContentWrapper
anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
Connections {
target: root
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}
}
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.shouldBeVisible || contentWindow.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close();
event.accepted = true;
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:color-picker"
HyprlandFocusGrab {
windows: [root.contentWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}
property string pickerTitle: I18n.tr("Choose Color")
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
property var onColorSelectedCallback: null
@@ -84,44 +90,27 @@ DankModal {
root.show();
}
function runNiriPicker() {
Proc.runCommand("niri-pick-color", ["niri", "msg", "pick-color"], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("niri msg pick-color exited with code:", exitCode);
root.show();
return;
}
const hexMatch = output.match(/Hex:\s*(#[0-9A-Fa-f]{6})/);
if (hexMatch) {
applyPickedColor(hexMatch[1]);
} else {
console.warn("Failed to parse niri pick-color output:", output);
root.show();
}
});
}
function pickColorFromScreen() {
hideInstant();
Proc.runCommand("check-hyprpicker", ["which", "hyprpicker"], (output, exitCode) => {
if (exitCode === 0) {
Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (hpOutput, hpCode) => {
if (hpCode !== 0) {
console.warn("hyprpicker exited with code:", hpCode);
root.show();
return;
}
applyPickedColor(hpOutput.trim());
});
Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => {
if (exitCode !== 0) {
console.warn("dms color pick exited with code:", exitCode);
root.show();
return;
}
if (CompositorService.isNiri) {
runNiriPicker();
return;
try {
const result = JSON.parse(output);
if (result.hex) {
applyPickedColor(result.hex);
} else {
console.warn("Failed to parse dms color pick output: missing hex");
root.show();
}
} catch (e) {
console.warn("Failed to parse dms color pick JSON:", e);
root.show();
}
console.warn("No color picker available");
root.show();
});
}, 0, Proc.noTimeout);
}
modalWidth: 680

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -16,6 +17,12 @@ DankModal {
modalWidth: _maxW
modalHeight: _maxH
onBackgroundClicked: close()
onOpened: () => Qt.callLater(() => modalFocusScope.forceActiveFocus())
HyprlandFocusGrab {
windows: [root.contentWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}
function scrollDown() {
if (!root.activeFlickable)
@@ -33,35 +40,25 @@ DankModal {
root.activeFlickable.contentY = newY;
}
content: Component {
FocusScope {
anchors.fill: parent
focus: true
modalFocusScope.Keys.onPressed: event => {
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
scrollDown();
event.accepted = true;
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
scrollUp();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
scrollDown();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
scrollUp();
event.accepted = true;
}
}
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_J:
if (!(event.modifiers & Qt.ControlModifier))
return;
root.scrollDown();
event.accepted = true;
break;
case Qt.Key_K:
if (!(event.modifiers & Qt.ControlModifier))
return;
root.scrollUp();
event.accepted = true;
break;
case Qt.Key_Down:
root.scrollDown();
event.accepted = true;
break;
case Qt.Key_Up:
root.scrollUp();
event.accepted = true;
break;
}
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
@@ -97,14 +94,12 @@ DankModal {
const bind = binds[i];
if (bind.subcat) {
hasSubcats = true;
if (!subcats[bind.subcat]) {
if (!subcats[bind.subcat])
subcats[bind.subcat] = [];
}
subcats[bind.subcat].push(bind);
} else {
if (!subcats["_root"]) {
if (!subcats["_root"])
subcats["_root"] = [];
}
subcats["_root"].push(bind);
}
}
@@ -121,12 +116,10 @@ DankModal {
function distributeCategories(cols) {
const columns = [];
for (let i = 0; i < cols; i++) {
for (let i = 0; i < cols; i++)
columns.push([]);
}
for (let i = 0; i < categoryKeys.length; i++) {
for (let i = 0; i < categoryKeys.length; i++)
columns[i % cols].push(categoryKeys[i]);
}
return columns;
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:notification-center-modal"
HyprlandFocusGrab {
windows: [notificationModal.contentWindow]
active: notificationModal.useHyprlandFocusGrab && notificationModal.shouldHaveFocus
}
property bool notificationModalOpen: false
property var notificationListRef: null
@@ -55,6 +61,9 @@ DankModal {
modalHeight: 700
visible: false
onBackgroundClicked: hide()
onOpened: () => {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
onShouldBeVisibleChanged: shouldBeVisible => {
if (!shouldBeVisible) {
notificationModalOpen = false;
@@ -62,6 +71,7 @@ DankModal {
NotificationService.onOverlayClose();
}
}
modalFocusScope.Keys.onPressed: event => modalKeyboardController.handleKey(event)
NotificationKeyboardController {
id: modalKeyboardController
@@ -91,12 +101,10 @@ DankModal {
}
content: Component {
FocusScope {
Item {
id: notificationKeyHandler
anchors.fill: parent
focus: true
Keys.onPressed: event => modalKeyboardController.handleKey(event)
anchors.fill: parent
Column {
anchors.fill: parent

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