mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
Compare commits
77 Commits
0df47d2ce3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aedeab8a6a | ||
|
|
4d39169eb8 | ||
|
|
2ddc448150 | ||
|
|
f9a6b4ce2c | ||
|
|
22b2b69413 | ||
|
|
7f11632ea6 | ||
|
|
c0b4d5e2c2 | ||
|
|
2c23d0249c | ||
|
|
c3233fbf61 | ||
|
|
ecfc8e208c | ||
|
|
52d5e21fc4 | ||
|
|
6d0c56554f | ||
|
|
844e91dc9e | ||
|
|
1f00b5f577 | ||
|
|
2c48458384 | ||
|
|
ddda87c5a7 | ||
|
|
6b1bbca620 | ||
|
|
b5378e5d3c | ||
|
|
c69a55df29 | ||
|
|
5faa1a993a | ||
|
|
e56481f6d7 | ||
|
|
f9610d457c | ||
|
|
ae066f42a4 | ||
|
|
c60dd42fa7 | ||
|
|
7aac5ac5a1 | ||
|
|
ad0f3fa33b | ||
|
|
63d121b796 | ||
|
|
4291cfe82f | ||
|
|
f312868154 | ||
|
|
5b42d34ac8 | ||
|
|
397a8c275d | ||
|
|
2aabee453b | ||
|
|
185333a615 | ||
|
|
7d177eb1d4 | ||
|
|
705a84051d | ||
|
|
f6821f80e1 | ||
|
|
e7a6f5228d | ||
|
|
8161fd6acb | ||
|
|
2137920e81 | ||
|
|
879102599c | ||
|
|
44190f07fe | ||
|
|
a41487eb8f | ||
|
|
e1acaaa27c | ||
|
|
08a97aeff8 | ||
|
|
5b7302b46d | ||
|
|
34c0bba130 | ||
|
|
5a53447272 | ||
|
|
b6847289ff | ||
|
|
d22c43e08b | ||
|
|
d9deaa8d74 | ||
|
|
6c7776a9a6 | ||
|
|
62bd6e41ef | ||
|
|
293c7b42c6 | ||
|
|
788da62777 | ||
|
|
2c7f24a913 | ||
|
|
f236706d6a | ||
|
|
b097700591 | ||
|
|
50b112c9d6 | ||
|
|
c2f478b088 | ||
|
|
dccbb137d7 | ||
|
|
90f9940dbd | ||
|
|
f3f7cc9077 | ||
|
|
c331e2f39e | ||
|
|
1c7ebc4323 | ||
|
|
5f5427266f | ||
|
|
33e655becd | ||
|
|
0ea0602aec | ||
|
|
46effd2ca4 | ||
|
|
de055e8260 | ||
|
|
c3077304af | ||
|
|
e15135911f | ||
|
|
d430cae944 | ||
|
|
f92dc6f71b | ||
|
|
a679be68b1 | ||
|
|
c5c5ce8409 | ||
|
|
e7cb0d397e | ||
|
|
b84308cb49 |
@@ -37,7 +37,10 @@ if [[ -n "$STAGED_CORE_FILES" ]]; then
|
||||
|
||||
# Tests
|
||||
echo " Running tests..."
|
||||
go test ./... >/dev/null
|
||||
if ! go test ./... >/dev/null 2>&1; then
|
||||
echo "Tests failed! Run 'go test ./...' for details."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build checks
|
||||
echo " Building..."
|
||||
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
72
.github/workflows/run-obs.yml
vendored
72
.github/workflows/run-obs.yml
vendored
@@ -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
|
||||
|
||||
75
.github/workflows/run-ppa.yml
vendored
75
.github/workflows/run-ppa.yml
vendored
@@ -15,9 +15,70 @@ on:
|
||||
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
||||
|
||||
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
|
||||
|
||||
2
Makefile
2
Makefile
@@ -81,7 +81,7 @@ install: build install-bin install-shell install-completions install-systemd ins
|
||||
@echo ""
|
||||
@echo "Installation complete!"
|
||||
@echo ""
|
||||
@echo "=== The DMS Team! ==="
|
||||
@echo "=== Cheers, the DMS Team! ==="
|
||||
|
||||
# Uninstallation targets
|
||||
uninstall-bin:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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+
|
||||
|
||||
133
core/cmd/dms/commands_colorpicker.go
Normal file
133
core/cmd/dms/commands_colorpicker.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
colorOutputFmt string
|
||||
colorAutocopy bool
|
||||
colorNotify bool
|
||||
colorLowercase bool
|
||||
)
|
||||
|
||||
var colorCmd = &cobra.Command{
|
||||
Use: "color",
|
||||
Short: "Color utilities",
|
||||
Long: "Color utilities including picking colors from the screen",
|
||||
}
|
||||
|
||||
var colorPickCmd = &cobra.Command{
|
||||
Use: "pick",
|
||||
Short: "Pick a color from the screen",
|
||||
Long: `Pick a color from anywhere on your screen using an interactive color picker.
|
||||
|
||||
Click on any pixel to capture its color, or press Escape to cancel.
|
||||
|
||||
Output format flags (mutually exclusive, default: --hex):
|
||||
--hex - Hexadecimal (#RRGGBB)
|
||||
--rgb - RGB values (R G B)
|
||||
--hsl - HSL values (H S% L%)
|
||||
--hsv - HSV values (H S% V%)
|
||||
--cmyk - CMYK values (C% M% Y% K%)
|
||||
--json - JSON with all formats
|
||||
|
||||
Examples:
|
||||
dms color pick # Pick color, output as hex
|
||||
dms color pick --rgb # Output as RGB
|
||||
dms color pick --json # Output all formats as JSON
|
||||
dms color pick --hex -l # Output hex in lowercase
|
||||
dms color pick -a # Auto-copy result to clipboard`,
|
||||
Run: runColorPick,
|
||||
}
|
||||
|
||||
func init() {
|
||||
colorPickCmd.Flags().Bool("hex", false, "Output as hexadecimal (#RRGGBB)")
|
||||
colorPickCmd.Flags().Bool("rgb", false, "Output as RGB (R G B)")
|
||||
colorPickCmd.Flags().Bool("hsl", false, "Output as HSL (H S% L%)")
|
||||
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
||||
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
||||
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||
|
||||
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
|
||||
|
||||
colorCmd.AddCommand(colorPickCmd)
|
||||
}
|
||||
|
||||
func runColorPick(cmd *cobra.Command, args []string) {
|
||||
format := colorpicker.FormatHex // default
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
if rgb, _ := cmd.Flags().GetBool("rgb"); rgb {
|
||||
format = colorpicker.FormatRGB
|
||||
} else if hsl, _ := cmd.Flags().GetBool("hsl"); hsl {
|
||||
format = colorpicker.FormatHSL
|
||||
} else if hsv, _ := cmd.Flags().GetBool("hsv"); hsv {
|
||||
format = colorpicker.FormatHSV
|
||||
} else if cmyk, _ := cmd.Flags().GetBool("cmyk"); cmyk {
|
||||
format = colorpicker.FormatCMYK
|
||||
}
|
||||
|
||||
config := colorpicker.Config{
|
||||
Format: format,
|
||||
CustomFormat: colorOutputFmt,
|
||||
Lowercase: colorLowercase,
|
||||
Autocopy: colorAutocopy,
|
||||
Notify: colorNotify,
|
||||
}
|
||||
|
||||
picker := colorpicker.New(config)
|
||||
color, err := picker.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if color == nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var output string
|
||||
if jsonOutput {
|
||||
jsonStr, err := color.ToJSON()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
output = jsonStr
|
||||
} else {
|
||||
output = color.Format(config.Format, config.Lowercase, config.CustomFormat)
|
||||
}
|
||||
|
||||
if colorAutocopy {
|
||||
copyToClipboard(output)
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
fmt.Println(output)
|
||||
} else if color.IsDark() {
|
||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||
} else {
|
||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||
}
|
||||
}
|
||||
|
||||
func copyToClipboard(text string) {
|
||||
var cmd *exec.Cmd
|
||||
if _, err := exec.LookPath("wl-copy"); err == nil {
|
||||
cmd = exec.Command("wl-copy", text)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
|
||||
return
|
||||
}
|
||||
|
||||
_ = cmd.Run()
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -152,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 {
|
||||
@@ -421,5 +471,8 @@ func getCommonCommands() []*cobra.Command {
|
||||
keybindsCmd,
|
||||
greeterCmd,
|
||||
setupCmd,
|
||||
colorCmd,
|
||||
screenshotCmd,
|
||||
notifyActionCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,34 @@ var keybindsShowCmd = &cobra.Command{
|
||||
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)
|
||||
@@ -82,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))
|
||||
}
|
||||
|
||||
377
core/cmd/dms/commands_screenshot.go
Normal file
377
core/cmd/dms/commands_screenshot.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
ssOutputName string
|
||||
ssIncludeCursor bool
|
||||
ssFormat string
|
||||
ssQuality int
|
||||
ssOutputDir string
|
||||
ssFilename string
|
||||
ssNoClipboard bool
|
||||
ssNoFile bool
|
||||
ssNoNotify bool
|
||||
ssStdout bool
|
||||
)
|
||||
|
||||
var screenshotCmd = &cobra.Command{
|
||||
Use: "screenshot",
|
||||
Short: "Capture screenshots",
|
||||
Long: `Capture screenshots from Wayland displays.
|
||||
|
||||
Modes:
|
||||
region - Select a region interactively (default)
|
||||
full - Capture the focused output
|
||||
all - Capture all outputs combined
|
||||
output - Capture a specific output by name
|
||||
window - Capture the focused window (Hyprland only)
|
||||
last - Capture the last selected region
|
||||
|
||||
Output format (--format):
|
||||
png - PNG format (default)
|
||||
jpg/jpeg - JPEG format
|
||||
ppm - PPM format
|
||||
|
||||
Examples:
|
||||
dms screenshot # Region select, save file + clipboard
|
||||
dms screenshot full # Full screen of focused output
|
||||
dms screenshot all # All screens combined
|
||||
dms screenshot output -o DP-1 # Specific output
|
||||
dms screenshot window # Focused window (Hyprland)
|
||||
dms screenshot last # Last region (pre-selected)
|
||||
dms screenshot --no-clipboard # Save file only
|
||||
dms screenshot --no-file # Clipboard only
|
||||
dms screenshot --cursor # Include cursor
|
||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||
}
|
||||
|
||||
var ssRegionCmd = &cobra.Command{
|
||||
Use: "region",
|
||||
Short: "Select a region interactively",
|
||||
Run: runScreenshotRegion,
|
||||
}
|
||||
|
||||
var ssFullCmd = &cobra.Command{
|
||||
Use: "full",
|
||||
Short: "Capture the focused output",
|
||||
Run: runScreenshotFull,
|
||||
}
|
||||
|
||||
var ssAllCmd = &cobra.Command{
|
||||
Use: "all",
|
||||
Short: "Capture all outputs combined",
|
||||
Run: runScreenshotAll,
|
||||
}
|
||||
|
||||
var ssOutputCmd = &cobra.Command{
|
||||
Use: "output",
|
||||
Short: "Capture a specific output",
|
||||
Run: runScreenshotOutput,
|
||||
}
|
||||
|
||||
var ssLastCmd = &cobra.Command{
|
||||
Use: "last",
|
||||
Short: "Capture the last selected region",
|
||||
Long: `Capture the previously selected region without interactive selection.
|
||||
If no previous region exists, falls back to interactive selection.`,
|
||||
Run: runScreenshotLast,
|
||||
}
|
||||
|
||||
var ssWindowCmd = &cobra.Command{
|
||||
Use: "window",
|
||||
Short: "Capture the focused window",
|
||||
Long: `Capture the currently focused window.
|
||||
Currently only supported on Hyprland.`,
|
||||
Run: runScreenshotWindow,
|
||||
}
|
||||
|
||||
var ssListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List available outputs",
|
||||
Run: runScreenshotList,
|
||||
}
|
||||
|
||||
var notifyActionCmd = &cobra.Command{
|
||||
Use: "notify-action",
|
||||
Hidden: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
screenshot.RunNotifyActionListener(args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||
screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||
|
||||
screenshotCmd.AddCommand(ssRegionCmd)
|
||||
screenshotCmd.AddCommand(ssFullCmd)
|
||||
screenshotCmd.AddCommand(ssAllCmd)
|
||||
screenshotCmd.AddCommand(ssOutputCmd)
|
||||
screenshotCmd.AddCommand(ssLastCmd)
|
||||
screenshotCmd.AddCommand(ssWindowCmd)
|
||||
screenshotCmd.AddCommand(ssListCmd)
|
||||
|
||||
screenshotCmd.Run = runScreenshotRegion
|
||||
}
|
||||
|
||||
func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config := screenshot.DefaultConfig()
|
||||
config.Mode = mode
|
||||
config.OutputName = ssOutputName
|
||||
config.IncludeCursor = ssIncludeCursor
|
||||
config.Clipboard = !ssNoClipboard
|
||||
config.SaveFile = !ssNoFile
|
||||
config.Notify = !ssNoNotify
|
||||
config.Stdout = ssStdout
|
||||
|
||||
if ssOutputDir != "" {
|
||||
config.OutputDir = ssOutputDir
|
||||
}
|
||||
if ssFilename != "" {
|
||||
config.Filename = ssFilename
|
||||
}
|
||||
|
||||
switch strings.ToLower(ssFormat) {
|
||||
case "jpg", "jpeg":
|
||||
config.Format = screenshot.FormatJPEG
|
||||
case "ppm":
|
||||
config.Format = screenshot.FormatPPM
|
||||
default:
|
||||
config.Format = screenshot.FormatPNG
|
||||
}
|
||||
|
||||
if ssQuality < 1 {
|
||||
ssQuality = 1
|
||||
}
|
||||
if ssQuality > 100 {
|
||||
ssQuality = 100
|
||||
}
|
||||
config.Quality = ssQuality
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func runScreenshot(config screenshot.Config) {
|
||||
sc := screenshot.New(config)
|
||||
result, err := sc.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
defer result.Buffer.Close()
|
||||
|
||||
if result.YInverted {
|
||||
result.Buffer.FlipVertical()
|
||||
}
|
||||
|
||||
if config.Stdout {
|
||||
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var filePath string
|
||||
|
||||
if config.SaveFile {
|
||||
outputDir := config.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = screenshot.GetOutputDir()
|
||||
}
|
||||
|
||||
filename := config.Filename
|
||||
if filename == "" {
|
||||
filename = screenshot.GenerateFilename(config.Format)
|
||||
}
|
||||
|
||||
filePath = filepath.Join(outputDir, filename)
|
||||
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(filePath)
|
||||
}
|
||||
|
||||
if config.Clipboard {
|
||||
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if !config.SaveFile {
|
||||
fmt.Println("Copied to clipboard")
|
||||
}
|
||||
}
|
||||
|
||||
if config.Notify {
|
||||
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
||||
screenshot.SendNotification(screenshot.NotifyResult{
|
||||
FilePath: filePath,
|
||||
Clipboard: config.Clipboard,
|
||||
ImageData: thumbData,
|
||||
Width: thumbW,
|
||||
Height: thumbH,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||
var mimeType string
|
||||
var data bytes.Buffer
|
||||
|
||||
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||
|
||||
switch format {
|
||||
case screenshot.FormatJPEG:
|
||||
mimeType = "image/jpeg"
|
||||
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
mimeType = "image/png"
|
||||
if err := screenshot.EncodePNG(&data, img); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("wl-copy", "--type", mimeType)
|
||||
cmd.Stdin = &data
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||
|
||||
switch format {
|
||||
case screenshot.FormatJPEG:
|
||||
return screenshot.EncodeJPEG(os.Stdout, img, quality)
|
||||
default:
|
||||
return screenshot.EncodePNG(os.Stdout, img)
|
||||
}
|
||||
}
|
||||
|
||||
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
|
||||
srcW, srcH := buf.Width, buf.Height
|
||||
scale := 1.0
|
||||
if srcW > maxSize || srcH > maxSize {
|
||||
if srcW > srcH {
|
||||
scale = float64(maxSize) / float64(srcW)
|
||||
} else {
|
||||
scale = float64(maxSize) / float64(srcH)
|
||||
}
|
||||
}
|
||||
|
||||
dstW := int(float64(srcW) * scale)
|
||||
dstH := int(float64(srcH) * scale)
|
||||
if dstW < 1 {
|
||||
dstW = 1
|
||||
}
|
||||
if dstH < 1 {
|
||||
dstH = 1
|
||||
}
|
||||
|
||||
data := buf.Data()
|
||||
rgb := make([]byte, dstW*dstH*3)
|
||||
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
|
||||
|
||||
for y := 0; y < dstH; y++ {
|
||||
srcY := int(float64(y) / scale)
|
||||
if srcY >= srcH {
|
||||
srcY = srcH - 1
|
||||
}
|
||||
for x := 0; x < dstW; x++ {
|
||||
srcX := int(float64(x) / scale)
|
||||
if srcX >= srcW {
|
||||
srcX = srcW - 1
|
||||
}
|
||||
si := srcY*buf.Stride + srcX*4
|
||||
di := (y*dstW + x) * 3
|
||||
if si+2 < len(data) {
|
||||
if swapRB {
|
||||
rgb[di+0] = data[si+2]
|
||||
rgb[di+1] = data[si+1]
|
||||
rgb[di+2] = data[si+0]
|
||||
} else {
|
||||
rgb[di+0] = data[si+0]
|
||||
rgb[di+1] = data[si+1]
|
||||
rgb[di+2] = data[si+2]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rgb, dstW, dstH
|
||||
}
|
||||
|
||||
func runScreenshotRegion(cmd *cobra.Command, args []string) {
|
||||
config := getScreenshotConfig(screenshot.ModeRegion)
|
||||
runScreenshot(config)
|
||||
}
|
||||
|
||||
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
||||
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
||||
runScreenshot(config)
|
||||
}
|
||||
|
||||
func runScreenshotAll(cmd *cobra.Command, args []string) {
|
||||
config := getScreenshotConfig(screenshot.ModeAllScreens)
|
||||
runScreenshot(config)
|
||||
}
|
||||
|
||||
func runScreenshotOutput(cmd *cobra.Command, args []string) {
|
||||
if ssOutputName == "" && len(args) > 0 {
|
||||
ssOutputName = args[0]
|
||||
}
|
||||
if ssOutputName == "" {
|
||||
fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)")
|
||||
os.Exit(1)
|
||||
}
|
||||
config := getScreenshotConfig(screenshot.ModeOutput)
|
||||
runScreenshot(config)
|
||||
}
|
||||
|
||||
func runScreenshotLast(cmd *cobra.Command, args []string) {
|
||||
config := getScreenshotConfig(screenshot.ModeLastRegion)
|
||||
runScreenshot(config)
|
||||
}
|
||||
|
||||
func runScreenshotWindow(cmd *cobra.Command, args []string) {
|
||||
config := getScreenshotConfig(screenshot.ModeWindow)
|
||||
runScreenshot(config)
|
||||
}
|
||||
|
||||
func runScreenshotList(cmd *cobra.Command, args []string) {
|
||||
outputs, err := screenshot.ListOutputs()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, o := range outputs {
|
||||
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
|
||||
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
|
||||
}
|
||||
}
|
||||
306
core/internal/colorpicker/color.go
Normal file
306
core/internal/colorpicker/color.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package colorpicker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Color struct {
|
||||
R, G, B, A uint8
|
||||
}
|
||||
|
||||
type OutputFormat int
|
||||
|
||||
const (
|
||||
FormatHex OutputFormat = iota
|
||||
FormatRGB
|
||||
FormatHSL
|
||||
FormatHSV
|
||||
FormatCMYK
|
||||
)
|
||||
|
||||
func ParseFormat(s string) OutputFormat {
|
||||
switch strings.ToLower(s) {
|
||||
case "rgb":
|
||||
return FormatRGB
|
||||
case "hsl":
|
||||
return FormatHSL
|
||||
case "hsv":
|
||||
return FormatHSV
|
||||
case "cmyk":
|
||||
return FormatCMYK
|
||||
default:
|
||||
return FormatHex
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) ToHex(lowercase bool) string {
|
||||
if lowercase {
|
||||
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
|
||||
}
|
||||
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
|
||||
}
|
||||
|
||||
func (c Color) ToRGB() string {
|
||||
return fmt.Sprintf("%d %d %d", c.R, c.G, c.B)
|
||||
}
|
||||
|
||||
func (c Color) ToHSL() string {
|
||||
h, s, l := rgbToHSL(c.R, c.G, c.B)
|
||||
return fmt.Sprintf("%d %d%% %d%%", h, s, l)
|
||||
}
|
||||
|
||||
func (c Color) ToHSV() string {
|
||||
h, s, v := rgbToHSV(c.R, c.G, c.B)
|
||||
return fmt.Sprintf("%d %d%% %d%%", h, s, v)
|
||||
}
|
||||
|
||||
func (c Color) ToCMYK() string {
|
||||
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
|
||||
return fmt.Sprintf("%d%% %d%% %d%% %d%%", cy, m, y, k)
|
||||
}
|
||||
|
||||
func (c Color) Format(format OutputFormat, lowercase bool, customFmt string) string {
|
||||
if customFmt != "" {
|
||||
return c.formatCustom(format, customFmt)
|
||||
}
|
||||
|
||||
switch format {
|
||||
case FormatRGB:
|
||||
return c.ToRGB()
|
||||
case FormatHSL:
|
||||
return c.ToHSL()
|
||||
case FormatHSV:
|
||||
return c.ToHSV()
|
||||
case FormatCMYK:
|
||||
return c.ToCMYK()
|
||||
default:
|
||||
return c.ToHex(lowercase)
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) formatCustom(format OutputFormat, customFmt string) string {
|
||||
switch format {
|
||||
case FormatRGB:
|
||||
return replaceArgs(customFmt, c.R, c.G, c.B)
|
||||
case FormatHSL:
|
||||
h, s, l := rgbToHSL(c.R, c.G, c.B)
|
||||
return replaceArgs(customFmt, h, s, l)
|
||||
case FormatHSV:
|
||||
h, s, v := rgbToHSV(c.R, c.G, c.B)
|
||||
return replaceArgs(customFmt, h, s, v)
|
||||
case FormatCMYK:
|
||||
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
|
||||
return replaceArgs4(customFmt, cy, m, y, k)
|
||||
default:
|
||||
if strings.Contains(customFmt, "{0}") {
|
||||
r := fmt.Sprintf("%02X", c.R)
|
||||
g := fmt.Sprintf("%02X", c.G)
|
||||
b := fmt.Sprintf("%02X", c.B)
|
||||
return replaceArgsStr(customFmt, r, g, b)
|
||||
}
|
||||
return c.ToHex(false)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceArgs[T any](format string, a, b, c T) string {
|
||||
result := format
|
||||
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
|
||||
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
|
||||
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
|
||||
return result
|
||||
}
|
||||
|
||||
func replaceArgs4[T any](format string, a, b, c, d T) string {
|
||||
result := format
|
||||
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
|
||||
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
|
||||
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
|
||||
result = strings.ReplaceAll(result, "{3}", fmt.Sprintf("%v", d))
|
||||
return result
|
||||
}
|
||||
|
||||
func replaceArgsStr(format, a, b, c string) string {
|
||||
result := format
|
||||
result = strings.ReplaceAll(result, "{0}", a)
|
||||
result = strings.ReplaceAll(result, "{1}", b)
|
||||
result = strings.ReplaceAll(result, "{2}", c)
|
||||
return result
|
||||
}
|
||||
|
||||
func rgbToHSL(r, g, b uint8) (int, int, int) {
|
||||
rf := float64(r) / 255.0
|
||||
gf := float64(g) / 255.0
|
||||
bf := float64(b) / 255.0
|
||||
|
||||
maxVal := math.Max(rf, math.Max(gf, bf))
|
||||
minVal := math.Min(rf, math.Min(gf, bf))
|
||||
l := (maxVal + minVal) / 2
|
||||
|
||||
if maxVal == minVal {
|
||||
return 0, 0, int(math.Round(l * 100))
|
||||
}
|
||||
|
||||
d := maxVal - minVal
|
||||
var s float64
|
||||
if l > 0.5 {
|
||||
s = d / (2 - maxVal - minVal)
|
||||
} else {
|
||||
s = d / (maxVal + minVal)
|
||||
}
|
||||
|
||||
var h float64
|
||||
switch maxVal {
|
||||
case rf:
|
||||
h = (gf - bf) / d
|
||||
if gf < bf {
|
||||
h += 6
|
||||
}
|
||||
case gf:
|
||||
h = (bf-rf)/d + 2
|
||||
case bf:
|
||||
h = (rf-gf)/d + 4
|
||||
}
|
||||
h /= 6
|
||||
|
||||
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(l * 100))
|
||||
}
|
||||
|
||||
func rgbToHSV(r, g, b uint8) (int, int, int) {
|
||||
rf := float64(r) / 255.0
|
||||
gf := float64(g) / 255.0
|
||||
bf := float64(b) / 255.0
|
||||
|
||||
maxVal := math.Max(rf, math.Max(gf, bf))
|
||||
minVal := math.Min(rf, math.Min(gf, bf))
|
||||
v := maxVal
|
||||
d := maxVal - minVal
|
||||
|
||||
var s float64
|
||||
if maxVal != 0 {
|
||||
s = d / maxVal
|
||||
}
|
||||
|
||||
if maxVal == minVal {
|
||||
return 0, int(math.Round(s * 100)), int(math.Round(v * 100))
|
||||
}
|
||||
|
||||
var h float64
|
||||
switch maxVal {
|
||||
case rf:
|
||||
h = (gf - bf) / d
|
||||
if gf < bf {
|
||||
h += 6
|
||||
}
|
||||
case gf:
|
||||
h = (bf-rf)/d + 2
|
||||
case bf:
|
||||
h = (rf-gf)/d + 4
|
||||
}
|
||||
h /= 6
|
||||
|
||||
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(v * 100))
|
||||
}
|
||||
|
||||
func rgbToCMYK(r, g, b uint8) (int, int, int, int) {
|
||||
if r == 0 && g == 0 && b == 0 {
|
||||
return 0, 0, 0, 100
|
||||
}
|
||||
|
||||
rf := float64(r) / 255.0
|
||||
gf := float64(g) / 255.0
|
||||
bf := float64(b) / 255.0
|
||||
|
||||
k := 1 - math.Max(rf, math.Max(gf, bf))
|
||||
c := (1 - rf - k) / (1 - k)
|
||||
m := (1 - gf - k) / (1 - k)
|
||||
y := (1 - bf - k) / (1 - k)
|
||||
|
||||
return int(math.Round(c * 100)), int(math.Round(m * 100)), int(math.Round(y * 100)), int(math.Round(k * 100))
|
||||
}
|
||||
|
||||
func (c Color) Luminance() float64 {
|
||||
r := float64(c.R) / 255.0
|
||||
g := float64(c.G) / 255.0
|
||||
b := float64(c.B) / 255.0
|
||||
|
||||
if r <= 0.03928 {
|
||||
r = r / 12.92
|
||||
} else {
|
||||
r = math.Pow((r+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
if g <= 0.03928 {
|
||||
g = g / 12.92
|
||||
} else {
|
||||
g = math.Pow((g+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
if b <= 0.03928 {
|
||||
b = b / 12.92
|
||||
} else {
|
||||
b = math.Pow((b+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
return 0.2126*r + 0.7152*g + 0.0722*b
|
||||
}
|
||||
|
||||
func (c Color) IsDark() bool {
|
||||
return c.Luminance() < 0.179
|
||||
}
|
||||
|
||||
type ColorJSON struct {
|
||||
Hex string `json:"hex"`
|
||||
RGB struct {
|
||||
R int `json:"r"`
|
||||
G int `json:"g"`
|
||||
B int `json:"b"`
|
||||
} `json:"rgb"`
|
||||
HSL struct {
|
||||
H int `json:"h"`
|
||||
S int `json:"s"`
|
||||
L int `json:"l"`
|
||||
} `json:"hsl"`
|
||||
HSV struct {
|
||||
H int `json:"h"`
|
||||
S int `json:"s"`
|
||||
V int `json:"v"`
|
||||
} `json:"hsv"`
|
||||
CMYK struct {
|
||||
C int `json:"c"`
|
||||
M int `json:"m"`
|
||||
Y int `json:"y"`
|
||||
K int `json:"k"`
|
||||
} `json:"cmyk"`
|
||||
}
|
||||
|
||||
func (c Color) ToJSON() (string, error) {
|
||||
h, s, l := rgbToHSL(c.R, c.G, c.B)
|
||||
hv, sv, v := rgbToHSV(c.R, c.G, c.B)
|
||||
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
|
||||
|
||||
data := ColorJSON{
|
||||
Hex: c.ToHex(false),
|
||||
}
|
||||
data.RGB.R = int(c.R)
|
||||
data.RGB.G = int(c.G)
|
||||
data.RGB.B = int(c.B)
|
||||
data.HSL.H = h
|
||||
data.HSL.S = s
|
||||
data.HSL.L = l
|
||||
data.HSV.H = hv
|
||||
data.HSV.S = sv
|
||||
data.HSV.V = v
|
||||
data.CMYK.C = cy
|
||||
data.CMYK.M = m
|
||||
data.CMYK.Y = y
|
||||
data.CMYK.K = k
|
||||
|
||||
bytes, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
730
core/internal/colorpicker/picker.go
Normal file
730
core/internal/colorpicker/picker.go
Normal file
@@ -0,0 +1,730 @@
|
||||
package colorpicker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Format OutputFormat
|
||||
CustomFormat string
|
||||
Lowercase bool
|
||||
Autocopy bool
|
||||
Notify bool
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
wlOutput *client.Output
|
||||
name string
|
||||
globalName uint32
|
||||
x, y int32
|
||||
width int32
|
||||
height int32
|
||||
scale int32
|
||||
fractionalScale float64
|
||||
}
|
||||
|
||||
type LayerSurface struct {
|
||||
output *Output
|
||||
state *SurfaceState
|
||||
wlSurface *client.Surface
|
||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||
viewport *wp_viewporter.WpViewport
|
||||
wlPool *client.ShmPool
|
||||
wlBuffer *client.Buffer
|
||||
bufferBusy bool
|
||||
oldPool *client.ShmPool
|
||||
oldBuffer *client.Buffer
|
||||
scopyBuffer *client.Buffer
|
||||
configured bool
|
||||
hidden bool
|
||||
}
|
||||
|
||||
type Picker struct {
|
||||
config Config
|
||||
|
||||
display *client.Display
|
||||
registry *client.Registry
|
||||
ctx *client.Context
|
||||
|
||||
compositor *client.Compositor
|
||||
shm *client.Shm
|
||||
seat *client.Seat
|
||||
pointer *client.Pointer
|
||||
keyboard *client.Keyboard
|
||||
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||
viewporter *wp_viewporter.WpViewporter
|
||||
|
||||
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
||||
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
||||
|
||||
outputs map[uint32]*Output
|
||||
outputsMu sync.Mutex
|
||||
|
||||
surfaces []*LayerSurface
|
||||
activeSurface *LayerSurface
|
||||
|
||||
running bool
|
||||
pickedColor *Color
|
||||
err error
|
||||
}
|
||||
|
||||
func New(config Config) *Picker {
|
||||
return &Picker{
|
||||
config: config,
|
||||
outputs: make(map[uint32]*Output),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Picker) Run() (*Color, error) {
|
||||
if err := p.connect(); err != nil {
|
||||
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer p.cleanup()
|
||||
|
||||
if err := p.setupRegistry(); err != nil {
|
||||
return nil, fmt.Errorf("registry setup: %w", err)
|
||||
}
|
||||
|
||||
if err := p.roundtrip(); err != nil {
|
||||
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||
}
|
||||
|
||||
if p.screencopy == nil {
|
||||
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||
}
|
||||
|
||||
if p.layerShell == nil {
|
||||
return nil, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
|
||||
}
|
||||
|
||||
if p.seat == nil {
|
||||
return nil, fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
if err := p.roundtrip(); err != nil {
|
||||
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||
}
|
||||
|
||||
if err := p.createSurfaces(); err != nil {
|
||||
return nil, fmt.Errorf("create surfaces: %w", err)
|
||||
}
|
||||
|
||||
if err := p.roundtrip(); err != nil {
|
||||
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||
}
|
||||
|
||||
p.running = true
|
||||
for p.running {
|
||||
if err := p.ctx.Dispatch(); err != nil {
|
||||
p.err = err
|
||||
break
|
||||
}
|
||||
|
||||
p.checkDone()
|
||||
}
|
||||
|
||||
if p.err != nil {
|
||||
return nil, p.err
|
||||
}
|
||||
|
||||
return p.pickedColor, nil
|
||||
}
|
||||
|
||||
func (p *Picker) checkDone() {
|
||||
for _, ls := range p.surfaces {
|
||||
picked, cancelled := ls.state.IsDone()
|
||||
switch {
|
||||
case cancelled:
|
||||
p.running = false
|
||||
return
|
||||
case picked:
|
||||
color, ok := ls.state.PickColor()
|
||||
if ok {
|
||||
p.pickedColor = &color
|
||||
}
|
||||
p.running = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Picker) connect() error {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.display = display
|
||||
p.ctx = display.Context()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Picker) roundtrip() error {
|
||||
return wlhelpers.Roundtrip(p.display, p.ctx)
|
||||
}
|
||||
|
||||
func (p *Picker) setupRegistry() error {
|
||||
registry, err := p.display.GetRegistry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.registry = registry
|
||||
|
||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||
p.handleGlobal(e)
|
||||
})
|
||||
|
||||
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||
p.outputsMu.Lock()
|
||||
delete(p.outputs, e.Name)
|
||||
p.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case client.CompositorInterfaceName:
|
||||
compositor := client.NewCompositor(p.ctx)
|
||||
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
|
||||
p.compositor = compositor
|
||||
}
|
||||
|
||||
case client.ShmInterfaceName:
|
||||
shm := client.NewShm(p.ctx)
|
||||
if err := p.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||
p.shm = shm
|
||||
}
|
||||
|
||||
case client.SeatInterfaceName:
|
||||
seat := client.NewSeat(p.ctx)
|
||||
if err := p.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
|
||||
p.seat = seat
|
||||
p.setupInput()
|
||||
}
|
||||
|
||||
case client.OutputInterfaceName:
|
||||
output := client.NewOutput(p.ctx)
|
||||
version := e.Version
|
||||
if version > 4 {
|
||||
version = 4
|
||||
}
|
||||
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||
p.outputsMu.Lock()
|
||||
p.outputs[e.Name] = &Output{
|
||||
wlOutput: output,
|
||||
globalName: e.Name,
|
||||
scale: 1,
|
||||
fractionalScale: 1.0,
|
||||
}
|
||||
p.outputsMu.Unlock()
|
||||
p.setupOutputHandlers(e.Name, output)
|
||||
}
|
||||
|
||||
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
||||
version := e.Version
|
||||
if version > 4 {
|
||||
version = 4
|
||||
}
|
||||
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
||||
p.layerShell = layerShell
|
||||
}
|
||||
|
||||
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
||||
version := e.Version
|
||||
if version > 3 {
|
||||
version = 3
|
||||
}
|
||||
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
||||
p.screencopy = screencopy
|
||||
}
|
||||
|
||||
case wp_viewporter.WpViewporterInterfaceName:
|
||||
viewporter := wp_viewporter.NewWpViewporter(p.ctx)
|
||||
if err := p.registry.Bind(e.Name, e.Interface, e.Version, viewporter); err == nil {
|
||||
p.viewporter = viewporter
|
||||
}
|
||||
|
||||
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
||||
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
|
||||
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||
p.shortcutsInhibitMgr = mgr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
|
||||
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||
p.outputsMu.Lock()
|
||||
if o, ok := p.outputs[name]; ok {
|
||||
o.x = e.X
|
||||
o.y = e.Y
|
||||
}
|
||||
p.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||
return
|
||||
}
|
||||
p.outputsMu.Lock()
|
||||
if o, ok := p.outputs[name]; ok {
|
||||
o.width = e.Width
|
||||
o.height = e.Height
|
||||
}
|
||||
p.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||
p.outputsMu.Lock()
|
||||
if o, ok := p.outputs[name]; ok {
|
||||
o.scale = e.Factor
|
||||
o.fractionalScale = float64(e.Factor)
|
||||
}
|
||||
p.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||
p.outputsMu.Lock()
|
||||
if o, ok := p.outputs[name]; ok {
|
||||
o.name = e.Name
|
||||
}
|
||||
p.outputsMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Picker) createSurfaces() error {
|
||||
p.outputsMu.Lock()
|
||||
outputs := make([]*Output, 0, len(p.outputs))
|
||||
for _, o := range p.outputs {
|
||||
outputs = append(outputs, o)
|
||||
}
|
||||
p.outputsMu.Unlock()
|
||||
|
||||
for _, output := range outputs {
|
||||
ls, err := p.createLayerSurface(output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("output %s: %w", output.name, err)
|
||||
}
|
||||
p.surfaces = append(p.surfaces, ls)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
|
||||
surface, err := p.compositor.CreateSurface()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create surface: %w", err)
|
||||
}
|
||||
|
||||
layerSurf, err := p.layerShell.GetLayerSurface(
|
||||
surface,
|
||||
output.wlOutput,
|
||||
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
|
||||
"dms-colorpicker",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get layer surface: %w", err)
|
||||
}
|
||||
|
||||
ls := &LayerSurface{
|
||||
output: output,
|
||||
state: NewSurfaceState(p.config.Format, p.config.Lowercase),
|
||||
wlSurface: surface,
|
||||
layerSurf: layerSurf,
|
||||
hidden: true, // Start hidden, will show overlay when pointer enters
|
||||
}
|
||||
|
||||
if p.viewporter != nil {
|
||||
vp, err := p.viewporter.GetViewport(surface)
|
||||
if err == nil {
|
||||
ls.viewport = vp
|
||||
}
|
||||
}
|
||||
|
||||
if err := layerSurf.SetAnchor(
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
|
||||
); err != nil {
|
||||
log.Warn("failed to set layer anchor", "err", err)
|
||||
}
|
||||
if err := layerSurf.SetExclusiveZone(-1); err != nil {
|
||||
log.Warn("failed to set exclusive zone", "err", err)
|
||||
}
|
||||
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
|
||||
log.Warn("failed to set keyboard interactivity", "err", err)
|
||||
}
|
||||
|
||||
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
|
||||
if err := layerSurf.AckConfigure(e.Serial); err != nil {
|
||||
log.Warn("failed to ack configure", "err", err)
|
||||
}
|
||||
if err := ls.state.OnLayerConfigure(int(e.Width), int(e.Height)); err != nil {
|
||||
log.Warn("failed to handle layer configure", "err", err)
|
||||
}
|
||||
ls.configured = true
|
||||
|
||||
scale := p.computeSurfaceScale(ls)
|
||||
ls.state.SetScale(scale)
|
||||
|
||||
if !ls.state.IsReady() {
|
||||
p.captureForSurface(ls)
|
||||
} else {
|
||||
p.redrawSurface(ls)
|
||||
}
|
||||
|
||||
// Request shortcut inhibition once surface is configured
|
||||
p.ensureShortcutsInhibitor(ls)
|
||||
})
|
||||
|
||||
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
|
||||
p.running = false
|
||||
})
|
||||
|
||||
if err := surface.Commit(); err != nil {
|
||||
log.Warn("failed to commit surface", "err", err)
|
||||
}
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
|
||||
out := ls.output
|
||||
if out == nil || out.fractionalScale <= 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
scale := int32(math.Ceil(out.fractionalScale))
|
||||
if scale <= 0 {
|
||||
scale = 1
|
||||
}
|
||||
return scale
|
||||
}
|
||||
|
||||
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
|
||||
if p.shortcutsInhibitMgr == nil || p.seat == nil || p.shortcutsInhibitor != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inhibitor, err := p.shortcutsInhibitMgr.InhibitShortcuts(ls.wlSurface, p.seat)
|
||||
if err != nil {
|
||||
log.Debug("failed to create shortcuts inhibitor", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.shortcutsInhibitor = inhibitor
|
||||
|
||||
inhibitor.SetActiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1ActiveEvent) {
|
||||
log.Debug("shortcuts inhibitor active")
|
||||
})
|
||||
|
||||
inhibitor.SetInactiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1InactiveEvent) {
|
||||
log.Debug("shortcuts inhibitor deactivated by compositor")
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Picker) captureForSurface(ls *LayerSurface) {
|
||||
frame, err := p.screencopy.CaptureOutput(0, ls.output.wlOutput)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||
if err := ls.state.OnScreencopyBuffer(PixelFormat(e.Format), int(e.Width), int(e.Height), int(e.Stride)); err != nil {
|
||||
log.Error("failed to create screencopy buffer", "err", err)
|
||||
}
|
||||
})
|
||||
|
||||
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
|
||||
screenBuf := ls.state.ScreenBuffer()
|
||||
if screenBuf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pool, err := p.shm.CreatePool(screenBuf.Fd(), int32(screenBuf.Size()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wlBuffer, err := pool.CreateBuffer(0, int32(screenBuf.Width), int32(screenBuf.Height), int32(screenBuf.Stride), uint32(ls.state.screenFormat))
|
||||
if err != nil {
|
||||
pool.Destroy()
|
||||
return
|
||||
}
|
||||
|
||||
if ls.scopyBuffer != nil {
|
||||
ls.scopyBuffer.Destroy()
|
||||
}
|
||||
ls.scopyBuffer = wlBuffer
|
||||
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
|
||||
|
||||
if err := frame.Copy(wlBuffer); err != nil {
|
||||
log.Error("failed to copy frame", "err", err)
|
||||
}
|
||||
pool.Destroy()
|
||||
})
|
||||
|
||||
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
|
||||
ls.state.OnScreencopyFlags(e.Flags)
|
||||
})
|
||||
|
||||
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||
ls.state.OnScreencopyReady()
|
||||
scale := p.computeSurfaceScale(ls)
|
||||
ls.state.SetScale(scale)
|
||||
frame.Destroy()
|
||||
p.redrawSurface(ls)
|
||||
})
|
||||
|
||||
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
|
||||
frame.Destroy()
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||
var renderBuf *ShmBuffer
|
||||
if ls.hidden {
|
||||
renderBuf = ls.state.RedrawScreenOnly()
|
||||
} else {
|
||||
renderBuf = ls.state.Redraw()
|
||||
}
|
||||
if renderBuf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ls.oldBuffer != nil {
|
||||
ls.oldBuffer.Destroy()
|
||||
ls.oldBuffer = nil
|
||||
}
|
||||
if ls.oldPool != nil {
|
||||
ls.oldPool.Destroy()
|
||||
ls.oldPool = nil
|
||||
}
|
||||
|
||||
ls.oldPool = ls.wlPool
|
||||
ls.oldBuffer = ls.wlBuffer
|
||||
ls.wlPool = nil
|
||||
ls.wlBuffer = nil
|
||||
|
||||
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ls.wlPool = pool
|
||||
|
||||
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ls.wlBuffer = wlBuffer
|
||||
|
||||
lsRef := ls
|
||||
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||
lsRef.bufferBusy = false
|
||||
})
|
||||
ls.bufferBusy = true
|
||||
|
||||
logicalW, logicalH := ls.state.LogicalSize()
|
||||
if logicalW == 0 || logicalH == 0 {
|
||||
logicalW = int(ls.output.width)
|
||||
logicalH = int(ls.output.height)
|
||||
}
|
||||
|
||||
scale := ls.state.Scale()
|
||||
if scale <= 0 {
|
||||
scale = 1
|
||||
}
|
||||
|
||||
if ls.viewport != nil {
|
||||
srcW := float64(renderBuf.Width) / float64(scale)
|
||||
srcH := float64(renderBuf.Height) / float64(scale)
|
||||
_ = ls.viewport.SetSource(0, 0, srcW, srcH)
|
||||
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
|
||||
}
|
||||
_ = ls.wlSurface.SetBufferScale(scale)
|
||||
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||
_ = ls.wlSurface.Commit()
|
||||
|
||||
ls.state.SwapBuffers()
|
||||
}
|
||||
|
||||
func (p *Picker) hideSurface(ls *LayerSurface) {
|
||||
if ls == nil || ls.wlSurface == nil || ls.hidden {
|
||||
return
|
||||
}
|
||||
ls.hidden = true
|
||||
// Redraw without the crosshair overlay
|
||||
p.redrawSurface(ls)
|
||||
}
|
||||
|
||||
func (p *Picker) setupInput() {
|
||||
if p.seat == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
||||
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
|
||||
pointer, err := p.seat.GetPointer()
|
||||
if err == nil {
|
||||
p.pointer = pointer
|
||||
p.setupPointerHandlers()
|
||||
}
|
||||
}
|
||||
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
|
||||
keyboard, err := p.seat.GetKeyboard()
|
||||
if err == nil {
|
||||
p.keyboard = keyboard
|
||||
p.setupKeyboardHandlers()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Picker) setupPointerHandlers() {
|
||||
p.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
|
||||
if err := p.pointer.SetCursor(e.Serial, nil, 0, 0); err != nil {
|
||||
log.Debug("failed to hide cursor", "err", err)
|
||||
}
|
||||
|
||||
if e.Surface == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.activeSurface = nil
|
||||
surfaceID := e.Surface.ID()
|
||||
for _, ls := range p.surfaces {
|
||||
if ls.wlSurface.ID() == surfaceID {
|
||||
p.activeSurface = ls
|
||||
break
|
||||
}
|
||||
}
|
||||
if p.activeSurface == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if p.activeSurface.hidden {
|
||||
p.activeSurface.hidden = false
|
||||
}
|
||||
|
||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||
p.redrawSurface(p.activeSurface)
|
||||
})
|
||||
|
||||
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||
if e.Surface == nil {
|
||||
return
|
||||
}
|
||||
surfaceID := e.Surface.ID()
|
||||
for _, ls := range p.surfaces {
|
||||
if ls.wlSurface.ID() == surfaceID {
|
||||
p.hideSurface(ls)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
p.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
|
||||
if p.activeSurface == nil {
|
||||
return
|
||||
}
|
||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||
p.redrawSurface(p.activeSurface)
|
||||
})
|
||||
|
||||
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||
if p.activeSurface == nil {
|
||||
return
|
||||
}
|
||||
p.activeSurface.state.OnPointerButton(e.Button, e.State)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Picker) setupKeyboardHandlers() {
|
||||
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||
for _, ls := range p.surfaces {
|
||||
ls.state.OnKey(e.Key, e.State)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Picker) cleanup() {
|
||||
for _, ls := range p.surfaces {
|
||||
if ls.scopyBuffer != nil {
|
||||
ls.scopyBuffer.Destroy()
|
||||
}
|
||||
if ls.oldBuffer != nil {
|
||||
ls.oldBuffer.Destroy()
|
||||
}
|
||||
if ls.oldPool != nil {
|
||||
ls.oldPool.Destroy()
|
||||
}
|
||||
if ls.wlBuffer != nil {
|
||||
ls.wlBuffer.Destroy()
|
||||
}
|
||||
if ls.wlPool != nil {
|
||||
ls.wlPool.Destroy()
|
||||
}
|
||||
if ls.viewport != nil {
|
||||
ls.viewport.Destroy()
|
||||
}
|
||||
if ls.layerSurf != nil {
|
||||
ls.layerSurf.Destroy()
|
||||
}
|
||||
if ls.wlSurface != nil {
|
||||
ls.wlSurface.Destroy()
|
||||
}
|
||||
if ls.state != nil {
|
||||
ls.state.Destroy()
|
||||
}
|
||||
}
|
||||
|
||||
if p.shortcutsInhibitor != nil {
|
||||
if err := p.shortcutsInhibitor.Destroy(); err != nil {
|
||||
log.Debug("failed to destroy shortcuts inhibitor", "err", err)
|
||||
}
|
||||
p.shortcutsInhibitor = nil
|
||||
}
|
||||
|
||||
if p.shortcutsInhibitMgr != nil {
|
||||
if err := p.shortcutsInhibitMgr.Destroy(); err != nil {
|
||||
log.Debug("failed to destroy shortcuts inhibit manager", "err", err)
|
||||
}
|
||||
p.shortcutsInhibitMgr = nil
|
||||
}
|
||||
|
||||
if p.viewporter != nil {
|
||||
p.viewporter.Destroy()
|
||||
}
|
||||
|
||||
if p.screencopy != nil {
|
||||
p.screencopy.Destroy()
|
||||
}
|
||||
|
||||
if p.pointer != nil {
|
||||
p.pointer.Release()
|
||||
}
|
||||
|
||||
if p.keyboard != nil {
|
||||
p.keyboard.Release()
|
||||
}
|
||||
|
||||
if p.display != nil {
|
||||
p.ctx.Close()
|
||||
}
|
||||
}
|
||||
40
core/internal/colorpicker/shm.go
Normal file
40
core/internal/colorpicker/shm.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package colorpicker
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||
|
||||
type ShmBuffer = shm.Buffer
|
||||
|
||||
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||
return shm.CreateBuffer(width, height, stride)
|
||||
}
|
||||
|
||||
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
|
||||
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
|
||||
}
|
||||
|
||||
func GetPixelColorWithFormat(buf *ShmBuffer, x, y int, format PixelFormat) Color {
|
||||
if x < 0 || x >= buf.Width || y < 0 || y >= buf.Height {
|
||||
return Color{}
|
||||
}
|
||||
|
||||
data := buf.Data()
|
||||
offset := y*buf.Stride + x*4
|
||||
if offset+3 >= len(data) {
|
||||
return Color{}
|
||||
}
|
||||
|
||||
if format == FormatABGR8888 || format == FormatXBGR8888 {
|
||||
return Color{
|
||||
R: data[offset],
|
||||
G: data[offset+1],
|
||||
B: data[offset+2],
|
||||
A: data[offset+3],
|
||||
}
|
||||
}
|
||||
return Color{
|
||||
B: data[offset],
|
||||
G: data[offset+1],
|
||||
R: data[offset+2],
|
||||
A: data[offset+3],
|
||||
}
|
||||
}
|
||||
1119
core/internal/colorpicker/state.go
Normal file
1119
core/internal/colorpicker/state.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -110,7 +110,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
5
core/internal/config/embedded/niri-alttab.kdl
Normal file
5
core/internal/config/embedded/niri-alttab.kdl
Normal file
@@ -0,0 +1,5 @@
|
||||
recent-windows {
|
||||
highlight {
|
||||
corner-radius 12
|
||||
}
|
||||
}
|
||||
195
core/internal/config/embedded/niri-binds.kdl
Normal file
195
core/internal/config/embedded/niri-binds.kdl
Normal file
@@ -0,0 +1,195 @@
|
||||
binds {
|
||||
// === System & Overview ===
|
||||
Mod+D repeat=false { toggle-overview; }
|
||||
Mod+Tab repeat=false { toggle-overview; }
|
||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||
|
||||
// === Application Launchers ===
|
||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
|
||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||
}
|
||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
||||
}
|
||||
Mod+M hotkey-overlay-title="Task Manager" {
|
||||
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||
}
|
||||
Mod+Comma hotkey-overlay-title="Settings" {
|
||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||
}
|
||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
||||
}
|
||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||
|
||||
// === Security ===
|
||||
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
|
||||
spawn "dms" "ipc" "call" "lock" "lock";
|
||||
}
|
||||
Mod+Shift+E { quit; }
|
||||
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
||||
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||
}
|
||||
|
||||
// === Audio Controls ===
|
||||
XF86AudioRaiseVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
||||
}
|
||||
XF86AudioLowerVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
||||
}
|
||||
XF86AudioMute allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "mute";
|
||||
}
|
||||
XF86AudioMicMute allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||
}
|
||||
|
||||
// === Brightness Controls ===
|
||||
XF86MonBrightnessUp allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
||||
}
|
||||
XF86MonBrightnessDown allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
||||
}
|
||||
|
||||
// === Window Management ===
|
||||
Mod+Q repeat=false { close-window; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
Mod+Shift+T { toggle-window-floating; }
|
||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||
Mod+W { toggle-column-tabbed-display; }
|
||||
|
||||
// === Focus Navigation ===
|
||||
Mod+Left { focus-column-left; }
|
||||
Mod+Down { focus-window-down; }
|
||||
Mod+Up { focus-window-up; }
|
||||
Mod+Right { focus-column-right; }
|
||||
Mod+H { focus-column-left; }
|
||||
Mod+J { focus-window-down; }
|
||||
Mod+K { focus-window-up; }
|
||||
Mod+L { focus-column-right; }
|
||||
|
||||
// === Window Movement ===
|
||||
Mod+Shift+Left { move-column-left; }
|
||||
Mod+Shift+Down { move-window-down; }
|
||||
Mod+Shift+Up { move-window-up; }
|
||||
Mod+Shift+Right { move-column-right; }
|
||||
Mod+Shift+H { move-column-left; }
|
||||
Mod+Shift+J { move-window-down; }
|
||||
Mod+Shift+K { move-window-up; }
|
||||
Mod+Shift+L { move-column-right; }
|
||||
|
||||
// === Column Navigation ===
|
||||
Mod+Home { focus-column-first; }
|
||||
Mod+End { focus-column-last; }
|
||||
Mod+Ctrl+Home { move-column-to-first; }
|
||||
Mod+Ctrl+End { move-column-to-last; }
|
||||
|
||||
// === Monitor Navigation ===
|
||||
Mod+Ctrl+Left { focus-monitor-left; }
|
||||
//Mod+Ctrl+Down { focus-monitor-down; }
|
||||
//Mod+Ctrl+Up { focus-monitor-up; }
|
||||
Mod+Ctrl+Right { focus-monitor-right; }
|
||||
Mod+Ctrl+H { focus-monitor-left; }
|
||||
Mod+Ctrl+J { focus-monitor-down; }
|
||||
Mod+Ctrl+K { focus-monitor-up; }
|
||||
Mod+Ctrl+L { focus-monitor-right; }
|
||||
|
||||
// === Move to Monitor ===
|
||||
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
|
||||
|
||||
// === Workspace Navigation ===
|
||||
Mod+Page_Down { focus-workspace-down; }
|
||||
Mod+Page_Up { focus-workspace-up; }
|
||||
Mod+U { focus-workspace-down; }
|
||||
Mod+I { focus-workspace-up; }
|
||||
Mod+Ctrl+Down { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+Up { move-column-to-workspace-up; }
|
||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||
|
||||
// === Move Workspaces ===
|
||||
Mod+Shift+Page_Down { move-workspace-down; }
|
||||
Mod+Shift+Page_Up { move-workspace-up; }
|
||||
Mod+Shift+U { move-workspace-down; }
|
||||
Mod+Shift+I { move-workspace-up; }
|
||||
|
||||
// === Mouse Wheel Navigation ===
|
||||
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||
|
||||
Mod+WheelScrollRight { focus-column-right; }
|
||||
Mod+WheelScrollLeft { focus-column-left; }
|
||||
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
||||
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
||||
|
||||
Mod+Shift+WheelScrollDown { focus-column-right; }
|
||||
Mod+Shift+WheelScrollUp { focus-column-left; }
|
||||
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
||||
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
||||
|
||||
// === Numbered Workspaces ===
|
||||
Mod+1 { focus-workspace 1; }
|
||||
Mod+2 { focus-workspace 2; }
|
||||
Mod+3 { focus-workspace 3; }
|
||||
Mod+4 { focus-workspace 4; }
|
||||
Mod+5 { focus-workspace 5; }
|
||||
Mod+6 { focus-workspace 6; }
|
||||
Mod+7 { focus-workspace 7; }
|
||||
Mod+8 { focus-workspace 8; }
|
||||
Mod+9 { focus-workspace 9; }
|
||||
|
||||
// === Move to Numbered Workspaces ===
|
||||
Mod+Shift+1 { move-column-to-workspace 1; }
|
||||
Mod+Shift+2 { move-column-to-workspace 2; }
|
||||
Mod+Shift+3 { move-column-to-workspace 3; }
|
||||
Mod+Shift+4 { move-column-to-workspace 4; }
|
||||
Mod+Shift+5 { move-column-to-workspace 5; }
|
||||
Mod+Shift+6 { move-column-to-workspace 6; }
|
||||
Mod+Shift+7 { move-column-to-workspace 7; }
|
||||
Mod+Shift+8 { move-column-to-workspace 8; }
|
||||
Mod+Shift+9 { move-column-to-workspace 9; }
|
||||
|
||||
// === Column Management ===
|
||||
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
Mod+BracketRight { consume-or-expel-window-right; }
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// === Sizing & Layout ===
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||
Mod+C { center-column; }
|
||||
Mod+Ctrl+C { center-visible-columns; }
|
||||
|
||||
// === Manual Sizing ===
|
||||
Mod+Minus { set-column-width "-10%"; }
|
||||
Mod+Equal { set-column-width "+10%"; }
|
||||
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||
|
||||
// === Screenshots ===
|
||||
XF86Launch1 { screenshot; }
|
||||
Ctrl+XF86Launch1 { screenshot-screen; }
|
||||
Alt+XF86Launch1 { screenshot-window; }
|
||||
Print { screenshot; }
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
// === System Controls ===
|
||||
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||
Mod+Shift+P { power-off-monitors; }
|
||||
}
|
||||
36
core/internal/config/embedded/niri-colors.kdl
Normal file
36
core/internal/config/embedded/niri-colors.kdl
Normal file
@@ -0,0 +1,36 @@
|
||||
layout {
|
||||
background-color "transparent"
|
||||
|
||||
focus-ring {
|
||||
active-color "#9dcbfb"
|
||||
inactive-color "#8c9199"
|
||||
urgent-color "#ffb4ab"
|
||||
}
|
||||
|
||||
border {
|
||||
active-color "#9dcbfb"
|
||||
inactive-color "#8c9199"
|
||||
urgent-color "#ffb4ab"
|
||||
}
|
||||
|
||||
shadow {
|
||||
color "#00000070"
|
||||
}
|
||||
|
||||
tab-indicator {
|
||||
active-color "#9dcbfb"
|
||||
inactive-color "#8c9199"
|
||||
urgent-color "#ffb4ab"
|
||||
}
|
||||
|
||||
insert-hint {
|
||||
color "#9dcbfb80"
|
||||
}
|
||||
}
|
||||
|
||||
recent-windows {
|
||||
highlight {
|
||||
active-color "#124a73"
|
||||
urgent-color "#ffb4ab"
|
||||
}
|
||||
}
|
||||
17
core/internal/config/embedded/niri-layout.kdl
Normal file
17
core/internal/config/embedded/niri-layout.kdl
Normal file
@@ -0,0 +1,17 @@
|
||||
layout {
|
||||
gaps 4
|
||||
|
||||
border {
|
||||
width 2
|
||||
}
|
||||
|
||||
focus-ring {
|
||||
width 2
|
||||
}
|
||||
}
|
||||
window-rule {
|
||||
geometry-corner-radius 12
|
||||
clip-to-geometry true
|
||||
tiled-state true
|
||||
draw-border-with-background false
|
||||
}
|
||||
@@ -214,210 +214,27 @@ window-rule {
|
||||
match app-id="zoom"
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,7 +70,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
|
||||
@@ -139,7 +138,12 @@ 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},
|
||||
@@ -148,24 +152,54 @@ func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
||||
"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"},
|
||||
|
||||
// Keep ghostty as manual (no OBS package yet)
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||
}
|
||||
|
||||
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 +272,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 +303,22 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
||||
}
|
||||
}
|
||||
|
||||
// OBS Packages
|
||||
obsPkgNames := d.extractPackageNames(obsPkgs)
|
||||
if len(obsPkgNames) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||
}
|
||||
if err := d.installAPTPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
@@ -297,8 +362,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 +372,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 +392,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -82,7 +82,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
|
||||
@@ -138,13 +137,12 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
"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 {
|
||||
@@ -156,13 +154,43 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
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 +322,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 +353,22 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Manual Builds
|
||||
// OBS Packages
|
||||
obsPkgNames := o.extractPackageNames(obsPkgs)
|
||||
if len(obsPkgNames) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||
}
|
||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
@@ -325,7 +382,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Configuration
|
||||
// Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
@@ -334,7 +391,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 +403,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 +433,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 {
|
||||
|
||||
@@ -82,7 +82,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
|
||||
@@ -151,6 +150,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},
|
||||
@@ -160,16 +163,16 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
||||
"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"},
|
||||
|
||||
// Keep ghostty as manual (no PPA available)
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
@@ -182,13 +185,42 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["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 +397,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] {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 := §ion.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 := §ion.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
// Generated by go-wayland-scanner
|
||||
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||
// XML file : internal/proto/xml/keyboard-shortcuts-inhibit-unstable-v1.xml
|
||||
//
|
||||
// keyboard_shortcuts_inhibit_unstable_v1 Protocol Copyright:
|
||||
//
|
||||
// Copyright © 2017 Red Hat Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice (including the next
|
||||
// paragraph) shall be included in all copies or substantial portions of the
|
||||
// Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package keyboard_shortcuts_inhibit
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
|
||||
// ZwpKeyboardShortcutsInhibitManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwpKeyboardShortcutsInhibitManagerV1InterfaceName = "zwp_keyboard_shortcuts_inhibit_manager_v1"
|
||||
|
||||
// ZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
|
||||
//
|
||||
// A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||
type ZwpKeyboardShortcutsInhibitManagerV1 struct {
|
||||
client.BaseProxy
|
||||
}
|
||||
|
||||
// NewZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
|
||||
//
|
||||
// A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||
func NewZwpKeyboardShortcutsInhibitManagerV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitManagerV1 {
|
||||
zwpKeyboardShortcutsInhibitManagerV1 := &ZwpKeyboardShortcutsInhibitManagerV1{}
|
||||
ctx.Register(zwpKeyboardShortcutsInhibitManagerV1)
|
||||
return zwpKeyboardShortcutsInhibitManagerV1
|
||||
}
|
||||
|
||||
// Destroy : destroy the keyboard shortcuts inhibitor object
|
||||
//
|
||||
// Destroy the keyboard shortcuts inhibitor manager.
|
||||
func (i *ZwpKeyboardShortcutsInhibitManagerV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// InhibitShortcuts : create a new keyboard shortcuts inhibitor object
|
||||
//
|
||||
// Create a new keyboard shortcuts inhibitor object associated with
|
||||
// the given surface for the given seat.
|
||||
//
|
||||
// If shortcuts are already inhibited for the specified seat and surface,
|
||||
// a protocol error "already_inhibited" is raised by the compositor.
|
||||
//
|
||||
// surface: the surface that inhibits the keyboard shortcuts behavior
|
||||
// seat: the wl_seat for which keyboard shortcuts should be disabled
|
||||
func (i *ZwpKeyboardShortcutsInhibitManagerV1) InhibitShortcuts(surface *client.Surface, seat *client.Seat) (*ZwpKeyboardShortcutsInhibitorV1, error) {
|
||||
id := NewZwpKeyboardShortcutsInhibitorV1(i.Context())
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8 + 4 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], seat.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return id, err
|
||||
}
|
||||
|
||||
type ZwpKeyboardShortcutsInhibitManagerV1Error uint32
|
||||
|
||||
// ZwpKeyboardShortcutsInhibitManagerV1Error :
|
||||
const (
|
||||
// ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited : the shortcuts are already inhibited for this surface
|
||||
ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited ZwpKeyboardShortcutsInhibitManagerV1Error = 0
|
||||
)
|
||||
|
||||
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Name() string {
|
||||
switch e {
|
||||
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
|
||||
return "already_inhibited"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Value() string {
|
||||
switch e {
|
||||
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
|
||||
return "0"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
// ZwpKeyboardShortcutsInhibitorV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwpKeyboardShortcutsInhibitorV1InterfaceName = "zwp_keyboard_shortcuts_inhibitor_v1"
|
||||
|
||||
// ZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
|
||||
//
|
||||
// A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||
// its own keyboard shortcuts when the associated surface has keyboard
|
||||
// focus. As a result, when the surface has keyboard focus on the given
|
||||
// seat, it will receive all key events originating from the specified
|
||||
// seat, even those which would normally be caught by the compositor for
|
||||
// its own shortcuts.
|
||||
//
|
||||
// The Wayland compositor is however under no obligation to disable
|
||||
// all of its shortcuts, and may keep some special key combo for its own
|
||||
// use, including but not limited to one allowing the user to forcibly
|
||||
// restore normal keyboard events routing in the case of an unwilling
|
||||
// client. The compositor may also use the same key combo to reactivate
|
||||
// an existing shortcut inhibitor that was previously deactivated on
|
||||
// user request.
|
||||
//
|
||||
// When the compositor restores its own keyboard shortcuts, an
|
||||
// "inactive" event is emitted to notify the client that the keyboard
|
||||
// shortcuts inhibitor is not effectively active for the surface and
|
||||
// seat any more, and the client should not expect to receive all
|
||||
// keyboard events.
|
||||
//
|
||||
// When the keyboard shortcuts inhibitor is inactive, the client has
|
||||
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||
//
|
||||
// The user can chose to re-enable a previously deactivated keyboard
|
||||
// shortcuts inhibitor using any mechanism the compositor may offer,
|
||||
// in which case the compositor will send an "active" event to notify
|
||||
// the client.
|
||||
//
|
||||
// If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||
// compositor will restore its own keyboard shortcuts but no "inactive"
|
||||
// event is emitted in this case.
|
||||
type ZwpKeyboardShortcutsInhibitorV1 struct {
|
||||
client.BaseProxy
|
||||
activeHandler ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc
|
||||
inactiveHandler ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc
|
||||
}
|
||||
|
||||
// NewZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
|
||||
//
|
||||
// A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||
// its own keyboard shortcuts when the associated surface has keyboard
|
||||
// focus. As a result, when the surface has keyboard focus on the given
|
||||
// seat, it will receive all key events originating from the specified
|
||||
// seat, even those which would normally be caught by the compositor for
|
||||
// its own shortcuts.
|
||||
//
|
||||
// The Wayland compositor is however under no obligation to disable
|
||||
// all of its shortcuts, and may keep some special key combo for its own
|
||||
// use, including but not limited to one allowing the user to forcibly
|
||||
// restore normal keyboard events routing in the case of an unwilling
|
||||
// client. The compositor may also use the same key combo to reactivate
|
||||
// an existing shortcut inhibitor that was previously deactivated on
|
||||
// user request.
|
||||
//
|
||||
// When the compositor restores its own keyboard shortcuts, an
|
||||
// "inactive" event is emitted to notify the client that the keyboard
|
||||
// shortcuts inhibitor is not effectively active for the surface and
|
||||
// seat any more, and the client should not expect to receive all
|
||||
// keyboard events.
|
||||
//
|
||||
// When the keyboard shortcuts inhibitor is inactive, the client has
|
||||
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||
//
|
||||
// The user can chose to re-enable a previously deactivated keyboard
|
||||
// shortcuts inhibitor using any mechanism the compositor may offer,
|
||||
// in which case the compositor will send an "active" event to notify
|
||||
// the client.
|
||||
//
|
||||
// If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||
// compositor will restore its own keyboard shortcuts but no "inactive"
|
||||
// event is emitted in this case.
|
||||
func NewZwpKeyboardShortcutsInhibitorV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitorV1 {
|
||||
zwpKeyboardShortcutsInhibitorV1 := &ZwpKeyboardShortcutsInhibitorV1{}
|
||||
ctx.Register(zwpKeyboardShortcutsInhibitorV1)
|
||||
return zwpKeyboardShortcutsInhibitorV1
|
||||
}
|
||||
|
||||
// Destroy : destroy the keyboard shortcuts inhibitor object
|
||||
//
|
||||
// Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
||||
func (i *ZwpKeyboardShortcutsInhibitorV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ZwpKeyboardShortcutsInhibitorV1ActiveEvent : shortcuts are inhibited
|
||||
//
|
||||
// This event indicates that the shortcut inhibitor is active.
|
||||
//
|
||||
// The compositor sends this event every time compositor shortcuts
|
||||
// are inhibited on behalf of the surface. When active, the client
|
||||
// may receive input events normally reserved by the compositor
|
||||
// (see zwp_keyboard_shortcuts_inhibitor_v1).
|
||||
//
|
||||
// This occurs typically when the initial request "inhibit_shortcuts"
|
||||
// first becomes active or when the user instructs the compositor to
|
||||
// re-enable and existing shortcuts inhibitor using any mechanism
|
||||
// offered by the compositor.
|
||||
type ZwpKeyboardShortcutsInhibitorV1ActiveEvent struct{}
|
||||
type ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1ActiveEvent)
|
||||
|
||||
// SetActiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1ActiveEvent
|
||||
func (i *ZwpKeyboardShortcutsInhibitorV1) SetActiveHandler(f ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc) {
|
||||
i.activeHandler = f
|
||||
}
|
||||
|
||||
// ZwpKeyboardShortcutsInhibitorV1InactiveEvent : shortcuts are restored
|
||||
//
|
||||
// This event indicates that the shortcuts inhibitor is inactive,
|
||||
// normal shortcuts processing is restored by the compositor.
|
||||
type ZwpKeyboardShortcutsInhibitorV1InactiveEvent struct{}
|
||||
type ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1InactiveEvent)
|
||||
|
||||
// SetInactiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1InactiveEvent
|
||||
func (i *ZwpKeyboardShortcutsInhibitorV1) SetInactiveHandler(f ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc) {
|
||||
i.inactiveHandler = f
|
||||
}
|
||||
|
||||
func (i *ZwpKeyboardShortcutsInhibitorV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if i.activeHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwpKeyboardShortcutsInhibitorV1ActiveEvent
|
||||
|
||||
i.activeHandler(e)
|
||||
case 1:
|
||||
if i.inactiveHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwpKeyboardShortcutsInhibitorV1InactiveEvent
|
||||
|
||||
i.inactiveHandler(e)
|
||||
}
|
||||
}
|
||||
792
core/internal/proto/wlr_layer_shell/layer_shell.go
Normal file
792
core/internal/proto/wlr_layer_shell/layer_shell.go
Normal file
@@ -0,0 +1,792 @@
|
||||
// Generated by go-wayland-scanner
|
||||
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||
// XML file : internal/proto/xml/wlr-layer-shell-unstable-v1.xml
|
||||
//
|
||||
// wlr_layer_shell_unstable_v1 Protocol Copyright:
|
||||
//
|
||||
// Copyright © 2017 Drew DeVault
|
||||
//
|
||||
// Permission to use, copy, modify, distribute, and sell this
|
||||
// software and its documentation for any purpose is hereby granted
|
||||
// without fee, provided that the above copyright notice appear in
|
||||
// all copies and that both that copyright notice and this permission
|
||||
// notice appear in supporting documentation, and that the name of
|
||||
// the copyright holders not be used in advertising or publicity
|
||||
// pertaining to distribution of the software without specific,
|
||||
// written prior permission. The copyright holders make no
|
||||
// representations about the suitability of this software for any
|
||||
// purpose. It is provided "as is" without express or implied
|
||||
// warranty.
|
||||
//
|
||||
// THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
// SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
// FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
// THIS SOFTWARE.
|
||||
|
||||
package wlr_layer_shell
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
xdg_shell "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/stable/xdg-shell"
|
||||
)
|
||||
|
||||
// ZwlrLayerShellV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwlrLayerShellV1InterfaceName = "zwlr_layer_shell_v1"
|
||||
|
||||
// ZwlrLayerShellV1 : create surfaces that are layers of the desktop
|
||||
//
|
||||
// Clients can use this interface to assign the surface_layer role to
|
||||
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||
// rendered with a defined z-depth respective to each other. They may also be
|
||||
// anchored to the edges and corners of a screen and specify input handling
|
||||
// semantics. This interface should be suitable for the implementation of
|
||||
// many desktop shell components, and a broad number of other applications
|
||||
// that interact with the desktop.
|
||||
type ZwlrLayerShellV1 struct {
|
||||
client.BaseProxy
|
||||
}
|
||||
|
||||
// NewZwlrLayerShellV1 : create surfaces that are layers of the desktop
|
||||
//
|
||||
// Clients can use this interface to assign the surface_layer role to
|
||||
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||
// rendered with a defined z-depth respective to each other. They may also be
|
||||
// anchored to the edges and corners of a screen and specify input handling
|
||||
// semantics. This interface should be suitable for the implementation of
|
||||
// many desktop shell components, and a broad number of other applications
|
||||
// that interact with the desktop.
|
||||
func NewZwlrLayerShellV1(ctx *client.Context) *ZwlrLayerShellV1 {
|
||||
zwlrLayerShellV1 := &ZwlrLayerShellV1{}
|
||||
ctx.Register(zwlrLayerShellV1)
|
||||
return zwlrLayerShellV1
|
||||
}
|
||||
|
||||
// GetLayerSurface : create a layer_surface from a surface
|
||||
//
|
||||
// Create a layer surface for an existing surface. This assigns the role of
|
||||
// layer_surface, or raises a protocol error if another role is already
|
||||
// assigned.
|
||||
//
|
||||
// Creating a layer surface from a wl_surface which has a buffer attached
|
||||
// or committed is a client error, and any attempts by a client to attach
|
||||
// or manipulate a buffer prior to the first layer_surface.configure call
|
||||
// must also be treated as errors.
|
||||
//
|
||||
// After creating a layer_surface object and setting it up, the client
|
||||
// must perform an initial commit without any buffer attached.
|
||||
// The compositor will reply with a layer_surface.configure event.
|
||||
// The client must acknowledge it and is then allowed to attach a buffer
|
||||
// to map the surface.
|
||||
//
|
||||
// You may pass NULL for output to allow the compositor to decide which
|
||||
// output to use. Generally this will be the one that the user most
|
||||
// recently interacted with.
|
||||
//
|
||||
// Clients can specify a namespace that defines the purpose of the layer
|
||||
// surface.
|
||||
//
|
||||
// layer: layer to add this surface to
|
||||
// namespace: namespace for the layer surface
|
||||
func (i *ZwlrLayerShellV1) GetLayerSurface(surface *client.Surface, output *client.Output, layer uint32, namespace string) (*ZwlrLayerSurfaceV1, error) {
|
||||
id := NewZwlrLayerSurfaceV1(i.Context())
|
||||
const opcode = 0
|
||||
namespaceLen := client.PaddedLen(len(namespace) + 1)
|
||||
_reqBufLen := 8 + 4 + 4 + 4 + 4 + (4 + namespaceLen)
|
||||
_reqBuf := make([]byte, _reqBufLen)
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||
l += 4
|
||||
if output == nil {
|
||||
client.PutUint32(_reqBuf[l:l+4], 0)
|
||||
l += 4
|
||||
} else {
|
||||
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||
l += 4
|
||||
}
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(layer))
|
||||
l += 4
|
||||
client.PutString(_reqBuf[l:l+(4+namespaceLen)], namespace)
|
||||
l += (4 + namespaceLen)
|
||||
err := i.Context().WriteMsg(_reqBuf, nil)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// Destroy : destroy the layer_shell object
|
||||
//
|
||||
// This request indicates that the client will not use the layer_shell
|
||||
// object any more. Objects that have been created through this instance
|
||||
// are not affected.
|
||||
func (i *ZwlrLayerShellV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type ZwlrLayerShellV1Error uint32
|
||||
|
||||
// ZwlrLayerShellV1Error :
|
||||
const (
|
||||
// ZwlrLayerShellV1ErrorRole : wl_surface has another role
|
||||
ZwlrLayerShellV1ErrorRole ZwlrLayerShellV1Error = 0
|
||||
// ZwlrLayerShellV1ErrorInvalidLayer : layer value is invalid
|
||||
ZwlrLayerShellV1ErrorInvalidLayer ZwlrLayerShellV1Error = 1
|
||||
// ZwlrLayerShellV1ErrorAlreadyConstructed : wl_surface has a buffer attached or committed
|
||||
ZwlrLayerShellV1ErrorAlreadyConstructed ZwlrLayerShellV1Error = 2
|
||||
)
|
||||
|
||||
func (e ZwlrLayerShellV1Error) Name() string {
|
||||
switch e {
|
||||
case ZwlrLayerShellV1ErrorRole:
|
||||
return "role"
|
||||
case ZwlrLayerShellV1ErrorInvalidLayer:
|
||||
return "invalid_layer"
|
||||
case ZwlrLayerShellV1ErrorAlreadyConstructed:
|
||||
return "already_constructed"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerShellV1Error) Value() string {
|
||||
switch e {
|
||||
case ZwlrLayerShellV1ErrorRole:
|
||||
return "0"
|
||||
case ZwlrLayerShellV1ErrorInvalidLayer:
|
||||
return "1"
|
||||
case ZwlrLayerShellV1ErrorAlreadyConstructed:
|
||||
return "2"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerShellV1Error) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
type ZwlrLayerShellV1Layer uint32
|
||||
|
||||
// ZwlrLayerShellV1Layer : available layers for surfaces
|
||||
//
|
||||
// These values indicate which layers a surface can be rendered in. They
|
||||
// are ordered by z depth, bottom-most first. Traditional shell surfaces
|
||||
// will typically be rendered between the bottom and top layers.
|
||||
// Fullscreen shell surfaces are typically rendered at the top layer.
|
||||
// Multiple surfaces can share a single layer, and ordering within a
|
||||
// single layer is undefined.
|
||||
const (
|
||||
ZwlrLayerShellV1LayerBackground ZwlrLayerShellV1Layer = 0
|
||||
ZwlrLayerShellV1LayerBottom ZwlrLayerShellV1Layer = 1
|
||||
ZwlrLayerShellV1LayerTop ZwlrLayerShellV1Layer = 2
|
||||
ZwlrLayerShellV1LayerOverlay ZwlrLayerShellV1Layer = 3
|
||||
)
|
||||
|
||||
func (e ZwlrLayerShellV1Layer) Name() string {
|
||||
switch e {
|
||||
case ZwlrLayerShellV1LayerBackground:
|
||||
return "background"
|
||||
case ZwlrLayerShellV1LayerBottom:
|
||||
return "bottom"
|
||||
case ZwlrLayerShellV1LayerTop:
|
||||
return "top"
|
||||
case ZwlrLayerShellV1LayerOverlay:
|
||||
return "overlay"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerShellV1Layer) Value() string {
|
||||
switch e {
|
||||
case ZwlrLayerShellV1LayerBackground:
|
||||
return "0"
|
||||
case ZwlrLayerShellV1LayerBottom:
|
||||
return "1"
|
||||
case ZwlrLayerShellV1LayerTop:
|
||||
return "2"
|
||||
case ZwlrLayerShellV1LayerOverlay:
|
||||
return "3"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerShellV1Layer) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
// ZwlrLayerSurfaceV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwlrLayerSurfaceV1InterfaceName = "zwlr_layer_surface_v1"
|
||||
|
||||
// ZwlrLayerSurfaceV1 : layer metadata interface
|
||||
//
|
||||
// An interface that may be implemented by a wl_surface, for surfaces that
|
||||
// are designed to be rendered as a layer of a stacked desktop-like
|
||||
// environment.
|
||||
//
|
||||
// Layer surface state (layer, size, anchor, exclusive zone,
|
||||
// margin, interactivity) is double-buffered, and will be applied at the
|
||||
// time wl_surface.commit of the corresponding wl_surface is called.
|
||||
//
|
||||
// Attaching a null buffer to a layer surface unmaps it.
|
||||
//
|
||||
// Unmapping a layer_surface means that the surface cannot be shown by the
|
||||
// compositor until it is explicitly mapped again. The layer_surface
|
||||
// returns to the state it had right after layer_shell.get_layer_surface.
|
||||
// The client can re-map the surface by performing a commit without any
|
||||
// buffer attached, waiting for a configure event and handling it as usual.
|
||||
type ZwlrLayerSurfaceV1 struct {
|
||||
client.BaseProxy
|
||||
configureHandler ZwlrLayerSurfaceV1ConfigureHandlerFunc
|
||||
closedHandler ZwlrLayerSurfaceV1ClosedHandlerFunc
|
||||
}
|
||||
|
||||
// NewZwlrLayerSurfaceV1 : layer metadata interface
|
||||
//
|
||||
// An interface that may be implemented by a wl_surface, for surfaces that
|
||||
// are designed to be rendered as a layer of a stacked desktop-like
|
||||
// environment.
|
||||
//
|
||||
// Layer surface state (layer, size, anchor, exclusive zone,
|
||||
// margin, interactivity) is double-buffered, and will be applied at the
|
||||
// time wl_surface.commit of the corresponding wl_surface is called.
|
||||
//
|
||||
// Attaching a null buffer to a layer surface unmaps it.
|
||||
//
|
||||
// Unmapping a layer_surface means that the surface cannot be shown by the
|
||||
// compositor until it is explicitly mapped again. The layer_surface
|
||||
// returns to the state it had right after layer_shell.get_layer_surface.
|
||||
// The client can re-map the surface by performing a commit without any
|
||||
// buffer attached, waiting for a configure event and handling it as usual.
|
||||
func NewZwlrLayerSurfaceV1(ctx *client.Context) *ZwlrLayerSurfaceV1 {
|
||||
zwlrLayerSurfaceV1 := &ZwlrLayerSurfaceV1{}
|
||||
ctx.Register(zwlrLayerSurfaceV1)
|
||||
return zwlrLayerSurfaceV1
|
||||
}
|
||||
|
||||
// SetSize : sets the size of the surface
|
||||
//
|
||||
// Sets the size of the surface in surface-local coordinates. The
|
||||
// compositor will display the surface centered with respect to its
|
||||
// anchors.
|
||||
//
|
||||
// If you pass 0 for either value, the compositor will assign it and
|
||||
// inform you of the assignment in the configure event. You must set your
|
||||
// anchor to opposite edges in the dimensions you omit; not doing so is a
|
||||
// protocol error. Both values are 0 by default.
|
||||
//
|
||||
// Size is double-buffered, see wl_surface.commit.
|
||||
func (i *ZwlrLayerSurfaceV1) SetSize(width, height uint32) error {
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(width))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetAnchor : configures the anchor point of the surface
|
||||
//
|
||||
// Requests that the compositor anchor the surface to the specified edges
|
||||
// and corners. If two orthogonal edges are specified (e.g. 'top' and
|
||||
// 'left'), then the anchor point will be the intersection of the edges
|
||||
// (e.g. the top left corner of the output); otherwise the anchor point
|
||||
// will be centered on that edge, or in the center if none is specified.
|
||||
//
|
||||
// Anchor is double-buffered, see wl_surface.commit.
|
||||
func (i *ZwlrLayerSurfaceV1) SetAnchor(anchor uint32) error {
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(anchor))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetExclusiveZone : configures the exclusive geometry of this surface
|
||||
//
|
||||
// Requests that the compositor avoids occluding an area with other
|
||||
// surfaces. The compositor's use of this information is
|
||||
// implementation-dependent - do not assume that this region will not
|
||||
// actually be occluded.
|
||||
//
|
||||
// A positive value is only meaningful if the surface is anchored to one
|
||||
// edge or an edge and both perpendicular edges. If the surface is not
|
||||
// anchored, anchored to only two perpendicular edges (a corner), anchored
|
||||
// to only two parallel edges or anchored to all edges, a positive value
|
||||
// will be treated the same as zero.
|
||||
//
|
||||
// A positive zone is the distance from the edge in surface-local
|
||||
// coordinates to consider exclusive.
|
||||
//
|
||||
// Surfaces that do not wish to have an exclusive zone may instead specify
|
||||
// how they should interact with surfaces that do. If set to zero, the
|
||||
// surface indicates that it would like to be moved to avoid occluding
|
||||
// surfaces with a positive exclusive zone. If set to -1, the surface
|
||||
// indicates that it would not like to be moved to accommodate for other
|
||||
// surfaces, and the compositor should extend it all the way to the edges
|
||||
// it is anchored to.
|
||||
//
|
||||
// For example, a panel might set its exclusive zone to 10, so that
|
||||
// maximized shell surfaces are not shown on top of it. A notification
|
||||
// might set its exclusive zone to 0, so that it is moved to avoid
|
||||
// occluding the panel, but shell surfaces are shown underneath it. A
|
||||
// wallpaper or lock screen might set their exclusive zone to -1, so that
|
||||
// they stretch below or over the panel.
|
||||
//
|
||||
// The default value is 0.
|
||||
//
|
||||
// Exclusive zone is double-buffered, see wl_surface.commit.
|
||||
func (i *ZwlrLayerSurfaceV1) SetExclusiveZone(zone int32) error {
|
||||
const opcode = 2
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(zone))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetMargin : sets a margin from the anchor point
|
||||
//
|
||||
// Requests that the surface be placed some distance away from the anchor
|
||||
// point on the output, in surface-local coordinates. Setting this value
|
||||
// for edges you are not anchored to has no effect.
|
||||
//
|
||||
// The exclusive zone includes the margin.
|
||||
//
|
||||
// Margin is double-buffered, see wl_surface.commit.
|
||||
func (i *ZwlrLayerSurfaceV1) SetMargin(top, right, bottom, left int32) error {
|
||||
const opcode = 3
|
||||
const _reqBufLen = 8 + 4 + 4 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(top))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(right))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(bottom))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(left))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetKeyboardInteractivity : requests keyboard events
|
||||
//
|
||||
// Set how keyboard events are delivered to this surface. By default,
|
||||
// layer shell surfaces do not receive keyboard events; this request can
|
||||
// be used to change this.
|
||||
//
|
||||
// This setting is inherited by child surfaces set by the get_popup
|
||||
// request.
|
||||
//
|
||||
// Layer surfaces receive pointer, touch, and tablet events normally. If
|
||||
// you do not want to receive them, set the input region on your surface
|
||||
// to an empty region.
|
||||
//
|
||||
// Keyboard interactivity is double-buffered, see wl_surface.commit.
|
||||
func (i *ZwlrLayerSurfaceV1) SetKeyboardInteractivity(keyboardInteractivity uint32) error {
|
||||
const opcode = 4
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(keyboardInteractivity))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPopup : assign this layer_surface as an xdg_popup parent
|
||||
//
|
||||
// This assigns an xdg_popup's parent to this layer_surface. This popup
|
||||
// should have been created via xdg_surface::get_popup with the parent set
|
||||
// to NULL, and this request must be invoked before committing the popup's
|
||||
// initial state.
|
||||
//
|
||||
// See the documentation of xdg_popup for more details about what an
|
||||
// xdg_popup is and how it is used.
|
||||
func (i *ZwlrLayerSurfaceV1) GetPopup(popup *xdg_shell.Popup) error {
|
||||
const opcode = 5
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], popup.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// AckConfigure : ack a configure event
|
||||
//
|
||||
// When a configure event is received, if a client commits the
|
||||
// surface in response to the configure event, then the client
|
||||
// must make an ack_configure request sometime before the commit
|
||||
// request, passing along the serial of the configure event.
|
||||
//
|
||||
// If the client receives multiple configure events before it
|
||||
// can respond to one, it only has to ack the last configure event.
|
||||
//
|
||||
// A client is not required to commit immediately after sending
|
||||
// an ack_configure request - it may even ack_configure several times
|
||||
// before its next surface commit.
|
||||
//
|
||||
// A client may send multiple ack_configure requests before committing, but
|
||||
// only the last request sent before a commit indicates which configure
|
||||
// event the client really is responding to.
|
||||
//
|
||||
// serial: the serial from the configure event
|
||||
func (i *ZwlrLayerSurfaceV1) AckConfigure(serial uint32) error {
|
||||
const opcode = 6
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(serial))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Destroy : destroy the layer_surface
|
||||
//
|
||||
// This request destroys the layer surface.
|
||||
func (i *ZwlrLayerSurfaceV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 7
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetLayer : change the layer of the surface
|
||||
//
|
||||
// Change the layer that the surface is rendered on.
|
||||
//
|
||||
// Layer is double-buffered, see wl_surface.commit.
|
||||
//
|
||||
// layer: layer to move this surface to
|
||||
func (i *ZwlrLayerSurfaceV1) SetLayer(layer uint32) error {
|
||||
const opcode = 8
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(layer))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetExclusiveEdge : set the edge the exclusive zone will be applied to
|
||||
//
|
||||
// Requests an edge for the exclusive zone to apply. The exclusive
|
||||
// edge will be automatically deduced from anchor points when possible,
|
||||
// but when the surface is anchored to a corner, it will be necessary
|
||||
// to set it explicitly to disambiguate, as it is not possible to deduce
|
||||
// which one of the two corner edges should be used.
|
||||
//
|
||||
// The edge must be one the surface is anchored to, otherwise the
|
||||
// invalid_exclusive_edge protocol error will be raised.
|
||||
func (i *ZwlrLayerSurfaceV1) SetExclusiveEdge(edge uint32) error {
|
||||
const opcode = 9
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(edge))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type ZwlrLayerSurfaceV1KeyboardInteractivity uint32
|
||||
|
||||
// ZwlrLayerSurfaceV1KeyboardInteractivity : types of keyboard interaction possible for a layer shell surface
|
||||
//
|
||||
// Types of keyboard interaction possible for layer shell surfaces. The
|
||||
// rationale for this is twofold: (1) some applications are not interested
|
||||
// in keyboard events and not allowing them to be focused can improve the
|
||||
// desktop experience; (2) some applications will want to take exclusive
|
||||
// keyboard focus.
|
||||
const (
|
||||
ZwlrLayerSurfaceV1KeyboardInteractivityNone ZwlrLayerSurfaceV1KeyboardInteractivity = 0
|
||||
ZwlrLayerSurfaceV1KeyboardInteractivityExclusive ZwlrLayerSurfaceV1KeyboardInteractivity = 1
|
||||
ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand ZwlrLayerSurfaceV1KeyboardInteractivity = 2
|
||||
)
|
||||
|
||||
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Name() string {
|
||||
switch e {
|
||||
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
|
||||
return "none"
|
||||
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
|
||||
return "exclusive"
|
||||
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
|
||||
return "on_demand"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Value() string {
|
||||
switch e {
|
||||
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
|
||||
return "0"
|
||||
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
|
||||
return "1"
|
||||
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
|
||||
return "2"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
type ZwlrLayerSurfaceV1Error uint32
|
||||
|
||||
// ZwlrLayerSurfaceV1Error :
|
||||
const (
|
||||
// ZwlrLayerSurfaceV1ErrorInvalidSurfaceState : provided surface state is invalid
|
||||
ZwlrLayerSurfaceV1ErrorInvalidSurfaceState ZwlrLayerSurfaceV1Error = 0
|
||||
// ZwlrLayerSurfaceV1ErrorInvalidSize : size is invalid
|
||||
ZwlrLayerSurfaceV1ErrorInvalidSize ZwlrLayerSurfaceV1Error = 1
|
||||
// ZwlrLayerSurfaceV1ErrorInvalidAnchor : anchor bitfield is invalid
|
||||
ZwlrLayerSurfaceV1ErrorInvalidAnchor ZwlrLayerSurfaceV1Error = 2
|
||||
// ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity : keyboard interactivity is invalid
|
||||
ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity ZwlrLayerSurfaceV1Error = 3
|
||||
// ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge : exclusive edge is invalid given the surface anchors
|
||||
ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge ZwlrLayerSurfaceV1Error = 4
|
||||
)
|
||||
|
||||
func (e ZwlrLayerSurfaceV1Error) Name() string {
|
||||
switch e {
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
|
||||
return "invalid_surface_state"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidSize:
|
||||
return "invalid_size"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
|
||||
return "invalid_anchor"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
|
||||
return "invalid_keyboard_interactivity"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
|
||||
return "invalid_exclusive_edge"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerSurfaceV1Error) Value() string {
|
||||
switch e {
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
|
||||
return "0"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidSize:
|
||||
return "1"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
|
||||
return "2"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
|
||||
return "3"
|
||||
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
|
||||
return "4"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerSurfaceV1Error) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
type ZwlrLayerSurfaceV1Anchor uint32
|
||||
|
||||
// ZwlrLayerSurfaceV1Anchor :
|
||||
const (
|
||||
// ZwlrLayerSurfaceV1AnchorTop : the top edge of the anchor rectangle
|
||||
ZwlrLayerSurfaceV1AnchorTop ZwlrLayerSurfaceV1Anchor = 1
|
||||
// ZwlrLayerSurfaceV1AnchorBottom : the bottom edge of the anchor rectangle
|
||||
ZwlrLayerSurfaceV1AnchorBottom ZwlrLayerSurfaceV1Anchor = 2
|
||||
// ZwlrLayerSurfaceV1AnchorLeft : the left edge of the anchor rectangle
|
||||
ZwlrLayerSurfaceV1AnchorLeft ZwlrLayerSurfaceV1Anchor = 4
|
||||
// ZwlrLayerSurfaceV1AnchorRight : the right edge of the anchor rectangle
|
||||
ZwlrLayerSurfaceV1AnchorRight ZwlrLayerSurfaceV1Anchor = 8
|
||||
)
|
||||
|
||||
func (e ZwlrLayerSurfaceV1Anchor) Name() string {
|
||||
switch e {
|
||||
case ZwlrLayerSurfaceV1AnchorTop:
|
||||
return "top"
|
||||
case ZwlrLayerSurfaceV1AnchorBottom:
|
||||
return "bottom"
|
||||
case ZwlrLayerSurfaceV1AnchorLeft:
|
||||
return "left"
|
||||
case ZwlrLayerSurfaceV1AnchorRight:
|
||||
return "right"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerSurfaceV1Anchor) Value() string {
|
||||
switch e {
|
||||
case ZwlrLayerSurfaceV1AnchorTop:
|
||||
return "1"
|
||||
case ZwlrLayerSurfaceV1AnchorBottom:
|
||||
return "2"
|
||||
case ZwlrLayerSurfaceV1AnchorLeft:
|
||||
return "4"
|
||||
case ZwlrLayerSurfaceV1AnchorRight:
|
||||
return "8"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrLayerSurfaceV1Anchor) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
// ZwlrLayerSurfaceV1ConfigureEvent : suggest a surface change
|
||||
//
|
||||
// The configure event asks the client to resize its surface.
|
||||
//
|
||||
// Clients should arrange their surface for the new states, and then send
|
||||
// an ack_configure request with the serial sent in this configure event at
|
||||
// some point before committing the new surface.
|
||||
//
|
||||
// The client is free to dismiss all but the last configure event it
|
||||
// received.
|
||||
//
|
||||
// The width and height arguments specify the size of the window in
|
||||
// surface-local coordinates.
|
||||
//
|
||||
// The size is a hint, in the sense that the client is free to ignore it if
|
||||
// it doesn't resize, pick a smaller size (to satisfy aspect ratio or
|
||||
// resize in steps of NxM pixels). If the client picks a smaller size and
|
||||
// is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
|
||||
// surface will be centered on this axis.
|
||||
//
|
||||
// If the width or height arguments are zero, it means the client should
|
||||
// decide its own window dimension.
|
||||
type ZwlrLayerSurfaceV1ConfigureEvent struct {
|
||||
Serial uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
}
|
||||
type ZwlrLayerSurfaceV1ConfigureHandlerFunc func(ZwlrLayerSurfaceV1ConfigureEvent)
|
||||
|
||||
// SetConfigureHandler : sets handler for ZwlrLayerSurfaceV1ConfigureEvent
|
||||
func (i *ZwlrLayerSurfaceV1) SetConfigureHandler(f ZwlrLayerSurfaceV1ConfigureHandlerFunc) {
|
||||
i.configureHandler = f
|
||||
}
|
||||
|
||||
// ZwlrLayerSurfaceV1ClosedEvent : surface should be closed
|
||||
//
|
||||
// The closed event is sent by the compositor when the surface will no
|
||||
// longer be shown. The output may have been destroyed or the user may
|
||||
// have asked for it to be removed. Further changes to the surface will be
|
||||
// ignored. The client should destroy the resource after receiving this
|
||||
// event, and create a new surface if they so choose.
|
||||
type ZwlrLayerSurfaceV1ClosedEvent struct{}
|
||||
type ZwlrLayerSurfaceV1ClosedHandlerFunc func(ZwlrLayerSurfaceV1ClosedEvent)
|
||||
|
||||
// SetClosedHandler : sets handler for ZwlrLayerSurfaceV1ClosedEvent
|
||||
func (i *ZwlrLayerSurfaceV1) SetClosedHandler(f ZwlrLayerSurfaceV1ClosedHandlerFunc) {
|
||||
i.closedHandler = f
|
||||
}
|
||||
|
||||
func (i *ZwlrLayerSurfaceV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if i.configureHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrLayerSurfaceV1ConfigureEvent
|
||||
l := 0
|
||||
e.Serial = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Width = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Height = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.configureHandler(e)
|
||||
case 1:
|
||||
if i.closedHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrLayerSurfaceV1ClosedEvent
|
||||
|
||||
i.closedHandler(e)
|
||||
}
|
||||
}
|
||||
532
core/internal/proto/wlr_screencopy/screencopy.go
Normal file
532
core/internal/proto/wlr_screencopy/screencopy.go
Normal file
@@ -0,0 +1,532 @@
|
||||
// Generated by go-wayland-scanner
|
||||
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||
// XML file : internal/proto/xml/wlr-screencopy-unstable-v1.xml
|
||||
//
|
||||
// wlr_screencopy_unstable_v1 Protocol Copyright:
|
||||
//
|
||||
// Copyright © 2018 Simon Ser
|
||||
// Copyright © 2019 Andri Yngvason
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice (including the next
|
||||
// paragraph) shall be included in all copies or substantial portions of the
|
||||
// Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package wlr_screencopy
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
|
||||
// ZwlrScreencopyManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwlrScreencopyManagerV1InterfaceName = "zwlr_screencopy_manager_v1"
|
||||
|
||||
// ZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
|
||||
//
|
||||
// This object is a manager which offers requests to start capturing from a
|
||||
// source.
|
||||
type ZwlrScreencopyManagerV1 struct {
|
||||
client.BaseProxy
|
||||
}
|
||||
|
||||
// NewZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
|
||||
//
|
||||
// This object is a manager which offers requests to start capturing from a
|
||||
// source.
|
||||
func NewZwlrScreencopyManagerV1(ctx *client.Context) *ZwlrScreencopyManagerV1 {
|
||||
zwlrScreencopyManagerV1 := &ZwlrScreencopyManagerV1{}
|
||||
ctx.Register(zwlrScreencopyManagerV1)
|
||||
return zwlrScreencopyManagerV1
|
||||
}
|
||||
|
||||
// CaptureOutput : capture an output
|
||||
//
|
||||
// Capture the next frame of an entire output.
|
||||
//
|
||||
// overlayCursor: composite cursor onto the frame
|
||||
func (i *ZwlrScreencopyManagerV1) CaptureOutput(overlayCursor int32, output *client.Output) (*ZwlrScreencopyFrameV1, error) {
|
||||
frame := NewZwlrScreencopyFrameV1(i.Context())
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8 + 4 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], frame.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return frame, err
|
||||
}
|
||||
|
||||
// CaptureOutputRegion : capture an output's region
|
||||
//
|
||||
// Capture the next frame of an output's region.
|
||||
//
|
||||
// The region is given in output logical coordinates, see
|
||||
// xdg_output.logical_size. The region will be clipped to the output's
|
||||
// extents.
|
||||
//
|
||||
// overlayCursor: composite cursor onto the frame
|
||||
func (i *ZwlrScreencopyManagerV1) CaptureOutputRegion(overlayCursor int32, output *client.Output, x, y, width, height int32) (*ZwlrScreencopyFrameV1, error) {
|
||||
frame := NewZwlrScreencopyFrameV1(i.Context())
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8 + 4 + 4 + 4 + 4 + 4 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], frame.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(x))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(y))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(width))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return frame, err
|
||||
}
|
||||
|
||||
// Destroy : destroy the manager
|
||||
//
|
||||
// All objects created by the manager will still remain valid, until their
|
||||
// appropriate destroy request has been called.
|
||||
func (i *ZwlrScreencopyManagerV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 2
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwlrScreencopyFrameV1InterfaceName = "zwlr_screencopy_frame_v1"
|
||||
|
||||
// ZwlrScreencopyFrameV1 : a frame ready for copy
|
||||
//
|
||||
// This object represents a single frame.
|
||||
//
|
||||
// When created, a series of buffer events will be sent, each representing a
|
||||
// supported buffer type. The "buffer_done" event is sent afterwards to
|
||||
// indicate that all supported buffer types have been enumerated. The client
|
||||
// will then be able to send a "copy" request. If the capture is successful,
|
||||
// the compositor will send a "flags" event followed by a "ready" event.
|
||||
//
|
||||
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||
// the "buffer" event is guaranteed to be sent.
|
||||
//
|
||||
// If the capture failed, the "failed" event is sent. This can happen anytime
|
||||
// before the "ready" event.
|
||||
//
|
||||
// Once either a "ready" or a "failed" event is received, the client should
|
||||
// destroy the frame.
|
||||
type ZwlrScreencopyFrameV1 struct {
|
||||
client.BaseProxy
|
||||
bufferHandler ZwlrScreencopyFrameV1BufferHandlerFunc
|
||||
flagsHandler ZwlrScreencopyFrameV1FlagsHandlerFunc
|
||||
readyHandler ZwlrScreencopyFrameV1ReadyHandlerFunc
|
||||
failedHandler ZwlrScreencopyFrameV1FailedHandlerFunc
|
||||
damageHandler ZwlrScreencopyFrameV1DamageHandlerFunc
|
||||
linuxDmabufHandler ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc
|
||||
bufferDoneHandler ZwlrScreencopyFrameV1BufferDoneHandlerFunc
|
||||
}
|
||||
|
||||
// NewZwlrScreencopyFrameV1 : a frame ready for copy
|
||||
//
|
||||
// This object represents a single frame.
|
||||
//
|
||||
// When created, a series of buffer events will be sent, each representing a
|
||||
// supported buffer type. The "buffer_done" event is sent afterwards to
|
||||
// indicate that all supported buffer types have been enumerated. The client
|
||||
// will then be able to send a "copy" request. If the capture is successful,
|
||||
// the compositor will send a "flags" event followed by a "ready" event.
|
||||
//
|
||||
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||
// the "buffer" event is guaranteed to be sent.
|
||||
//
|
||||
// If the capture failed, the "failed" event is sent. This can happen anytime
|
||||
// before the "ready" event.
|
||||
//
|
||||
// Once either a "ready" or a "failed" event is received, the client should
|
||||
// destroy the frame.
|
||||
func NewZwlrScreencopyFrameV1(ctx *client.Context) *ZwlrScreencopyFrameV1 {
|
||||
zwlrScreencopyFrameV1 := &ZwlrScreencopyFrameV1{}
|
||||
ctx.Register(zwlrScreencopyFrameV1)
|
||||
return zwlrScreencopyFrameV1
|
||||
}
|
||||
|
||||
// Copy : copy the frame
|
||||
//
|
||||
// Copy the frame to the supplied buffer. The buffer must have the
|
||||
// correct size, see zwlr_screencopy_frame_v1.buffer and
|
||||
// zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
|
||||
// supported format.
|
||||
//
|
||||
// If the frame is successfully copied, "flags" and "ready" events are
|
||||
// sent. Otherwise, a "failed" event is sent.
|
||||
func (i *ZwlrScreencopyFrameV1) Copy(buffer *client.Buffer) error {
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], buffer.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Destroy : delete this object, used or not
|
||||
//
|
||||
// Destroys the frame. This request can be sent at any time by the client.
|
||||
func (i *ZwlrScreencopyFrameV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// CopyWithDamage : copy the frame when it's damaged
|
||||
//
|
||||
// Same as copy, except it waits until there is damage to copy.
|
||||
func (i *ZwlrScreencopyFrameV1) CopyWithDamage(buffer *client.Buffer) error {
|
||||
const opcode = 2
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], buffer.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type ZwlrScreencopyFrameV1Error uint32
|
||||
|
||||
// ZwlrScreencopyFrameV1Error :
|
||||
const (
|
||||
// ZwlrScreencopyFrameV1ErrorAlreadyUsed : the object has already been used to copy a wl_buffer
|
||||
ZwlrScreencopyFrameV1ErrorAlreadyUsed ZwlrScreencopyFrameV1Error = 0
|
||||
// ZwlrScreencopyFrameV1ErrorInvalidBuffer : buffer attributes are invalid
|
||||
ZwlrScreencopyFrameV1ErrorInvalidBuffer ZwlrScreencopyFrameV1Error = 1
|
||||
)
|
||||
|
||||
func (e ZwlrScreencopyFrameV1Error) Name() string {
|
||||
switch e {
|
||||
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
|
||||
return "already_used"
|
||||
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
|
||||
return "invalid_buffer"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrScreencopyFrameV1Error) Value() string {
|
||||
switch e {
|
||||
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
|
||||
return "0"
|
||||
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
|
||||
return "1"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrScreencopyFrameV1Error) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
type ZwlrScreencopyFrameV1Flags uint32
|
||||
|
||||
// ZwlrScreencopyFrameV1Flags :
|
||||
const (
|
||||
// ZwlrScreencopyFrameV1FlagsYInvert : contents are y-inverted
|
||||
ZwlrScreencopyFrameV1FlagsYInvert ZwlrScreencopyFrameV1Flags = 1
|
||||
)
|
||||
|
||||
func (e ZwlrScreencopyFrameV1Flags) Name() string {
|
||||
switch e {
|
||||
case ZwlrScreencopyFrameV1FlagsYInvert:
|
||||
return "y_invert"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrScreencopyFrameV1Flags) Value() string {
|
||||
switch e {
|
||||
case ZwlrScreencopyFrameV1FlagsYInvert:
|
||||
return "1"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrScreencopyFrameV1Flags) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1BufferEvent : wl_shm buffer information
|
||||
//
|
||||
// Provides information about wl_shm buffer parameters that need to be
|
||||
// used for this frame. This event is sent once after the frame is created
|
||||
// if wl_shm buffers are supported.
|
||||
type ZwlrScreencopyFrameV1BufferEvent struct {
|
||||
Format uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
Stride uint32
|
||||
}
|
||||
type ZwlrScreencopyFrameV1BufferHandlerFunc func(ZwlrScreencopyFrameV1BufferEvent)
|
||||
|
||||
// SetBufferHandler : sets handler for ZwlrScreencopyFrameV1BufferEvent
|
||||
func (i *ZwlrScreencopyFrameV1) SetBufferHandler(f ZwlrScreencopyFrameV1BufferHandlerFunc) {
|
||||
i.bufferHandler = f
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1FlagsEvent : frame flags
|
||||
//
|
||||
// Provides flags about the frame. This event is sent once before the
|
||||
// "ready" event.
|
||||
type ZwlrScreencopyFrameV1FlagsEvent struct {
|
||||
Flags uint32
|
||||
}
|
||||
type ZwlrScreencopyFrameV1FlagsHandlerFunc func(ZwlrScreencopyFrameV1FlagsEvent)
|
||||
|
||||
// SetFlagsHandler : sets handler for ZwlrScreencopyFrameV1FlagsEvent
|
||||
func (i *ZwlrScreencopyFrameV1) SetFlagsHandler(f ZwlrScreencopyFrameV1FlagsHandlerFunc) {
|
||||
i.flagsHandler = f
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1ReadyEvent : indicates frame is available for reading
|
||||
//
|
||||
// Called as soon as the frame is copied, indicating it is available
|
||||
// for reading. This event includes the time at which the presentation took place.
|
||||
//
|
||||
// The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
|
||||
// each component being an unsigned 32-bit value. Whole seconds are in
|
||||
// tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
|
||||
// and the additional fractional part in tv_nsec as nanoseconds. Hence,
|
||||
// for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
|
||||
// may have an arbitrary offset at start.
|
||||
//
|
||||
// After receiving this event, the client should destroy the object.
|
||||
type ZwlrScreencopyFrameV1ReadyEvent struct {
|
||||
TvSecHi uint32
|
||||
TvSecLo uint32
|
||||
TvNsec uint32
|
||||
}
|
||||
type ZwlrScreencopyFrameV1ReadyHandlerFunc func(ZwlrScreencopyFrameV1ReadyEvent)
|
||||
|
||||
// SetReadyHandler : sets handler for ZwlrScreencopyFrameV1ReadyEvent
|
||||
func (i *ZwlrScreencopyFrameV1) SetReadyHandler(f ZwlrScreencopyFrameV1ReadyHandlerFunc) {
|
||||
i.readyHandler = f
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1FailedEvent : frame copy failed
|
||||
//
|
||||
// This event indicates that the attempted frame copy has failed.
|
||||
//
|
||||
// After receiving this event, the client should destroy the object.
|
||||
type ZwlrScreencopyFrameV1FailedEvent struct{}
|
||||
type ZwlrScreencopyFrameV1FailedHandlerFunc func(ZwlrScreencopyFrameV1FailedEvent)
|
||||
|
||||
// SetFailedHandler : sets handler for ZwlrScreencopyFrameV1FailedEvent
|
||||
func (i *ZwlrScreencopyFrameV1) SetFailedHandler(f ZwlrScreencopyFrameV1FailedHandlerFunc) {
|
||||
i.failedHandler = f
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1DamageEvent : carries the coordinates of the damaged region
|
||||
//
|
||||
// This event is sent right before the ready event when copy_with_damage is
|
||||
// requested. It may be generated multiple times for each copy_with_damage
|
||||
// request.
|
||||
//
|
||||
// The arguments describe a box around an area that has changed since the
|
||||
// last copy request that was derived from the current screencopy manager
|
||||
// instance.
|
||||
//
|
||||
// The union of all regions received between the call to copy_with_damage
|
||||
// and a ready event is the total damage since the prior ready event.
|
||||
type ZwlrScreencopyFrameV1DamageEvent struct {
|
||||
X uint32
|
||||
Y uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
}
|
||||
type ZwlrScreencopyFrameV1DamageHandlerFunc func(ZwlrScreencopyFrameV1DamageEvent)
|
||||
|
||||
// SetDamageHandler : sets handler for ZwlrScreencopyFrameV1DamageEvent
|
||||
func (i *ZwlrScreencopyFrameV1) SetDamageHandler(f ZwlrScreencopyFrameV1DamageHandlerFunc) {
|
||||
i.damageHandler = f
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1LinuxDmabufEvent : linux-dmabuf buffer information
|
||||
//
|
||||
// Provides information about linux-dmabuf buffer parameters that need to
|
||||
// be used for this frame. This event is sent once after the frame is
|
||||
// created if linux-dmabuf buffers are supported.
|
||||
type ZwlrScreencopyFrameV1LinuxDmabufEvent struct {
|
||||
Format uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
}
|
||||
type ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc func(ZwlrScreencopyFrameV1LinuxDmabufEvent)
|
||||
|
||||
// SetLinuxDmabufHandler : sets handler for ZwlrScreencopyFrameV1LinuxDmabufEvent
|
||||
func (i *ZwlrScreencopyFrameV1) SetLinuxDmabufHandler(f ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc) {
|
||||
i.linuxDmabufHandler = f
|
||||
}
|
||||
|
||||
// ZwlrScreencopyFrameV1BufferDoneEvent : all buffer types reported
|
||||
//
|
||||
// This event is sent once after all buffer events have been sent.
|
||||
//
|
||||
// The client should proceed to create a buffer of one of the supported
|
||||
// types, and send a "copy" request.
|
||||
type ZwlrScreencopyFrameV1BufferDoneEvent struct{}
|
||||
type ZwlrScreencopyFrameV1BufferDoneHandlerFunc func(ZwlrScreencopyFrameV1BufferDoneEvent)
|
||||
|
||||
// SetBufferDoneHandler : sets handler for ZwlrScreencopyFrameV1BufferDoneEvent
|
||||
func (i *ZwlrScreencopyFrameV1) SetBufferDoneHandler(f ZwlrScreencopyFrameV1BufferDoneHandlerFunc) {
|
||||
i.bufferDoneHandler = f
|
||||
}
|
||||
|
||||
func (i *ZwlrScreencopyFrameV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if i.bufferHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrScreencopyFrameV1BufferEvent
|
||||
l := 0
|
||||
e.Format = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Width = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Height = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Stride = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.bufferHandler(e)
|
||||
case 1:
|
||||
if i.flagsHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrScreencopyFrameV1FlagsEvent
|
||||
l := 0
|
||||
e.Flags = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.flagsHandler(e)
|
||||
case 2:
|
||||
if i.readyHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrScreencopyFrameV1ReadyEvent
|
||||
l := 0
|
||||
e.TvSecHi = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.TvSecLo = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.TvNsec = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.readyHandler(e)
|
||||
case 3:
|
||||
if i.failedHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrScreencopyFrameV1FailedEvent
|
||||
|
||||
i.failedHandler(e)
|
||||
case 4:
|
||||
if i.damageHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrScreencopyFrameV1DamageEvent
|
||||
l := 0
|
||||
e.X = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Y = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Width = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Height = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.damageHandler(e)
|
||||
case 5:
|
||||
if i.linuxDmabufHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrScreencopyFrameV1LinuxDmabufEvent
|
||||
l := 0
|
||||
e.Format = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Width = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Height = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.linuxDmabufHandler(e)
|
||||
case 6:
|
||||
if i.bufferDoneHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrScreencopyFrameV1BufferDoneEvent
|
||||
|
||||
i.bufferDoneHandler(e)
|
||||
}
|
||||
}
|
||||
399
core/internal/proto/wp_viewporter/viewporter.go
Normal file
399
core/internal/proto/wp_viewporter/viewporter.go
Normal file
@@ -0,0 +1,399 @@
|
||||
// Generated by go-wayland-scanner
|
||||
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||
// XML file : /tmp/viewporter.xml
|
||||
//
|
||||
// viewporter Protocol Copyright:
|
||||
//
|
||||
// Copyright © 2013-2016 Collabora, Ltd.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice (including the next
|
||||
// paragraph) shall be included in all copies or substantial portions of the
|
||||
// Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
package wp_viewporter
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
|
||||
// WpViewporterInterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const WpViewporterInterfaceName = "wp_viewporter"
|
||||
|
||||
// WpViewporter : surface cropping and scaling
|
||||
//
|
||||
// The global interface exposing surface cropping and scaling
|
||||
// capabilities is used to instantiate an interface extension for a
|
||||
// wl_surface object. This extended interface will then allow
|
||||
// cropping and scaling the surface contents, effectively
|
||||
// disconnecting the direct relationship between the buffer and the
|
||||
// surface size.
|
||||
type WpViewporter struct {
|
||||
client.BaseProxy
|
||||
}
|
||||
|
||||
// NewWpViewporter : surface cropping and scaling
|
||||
//
|
||||
// The global interface exposing surface cropping and scaling
|
||||
// capabilities is used to instantiate an interface extension for a
|
||||
// wl_surface object. This extended interface will then allow
|
||||
// cropping and scaling the surface contents, effectively
|
||||
// disconnecting the direct relationship between the buffer and the
|
||||
// surface size.
|
||||
func NewWpViewporter(ctx *client.Context) *WpViewporter {
|
||||
wpViewporter := &WpViewporter{}
|
||||
ctx.Register(wpViewporter)
|
||||
return wpViewporter
|
||||
}
|
||||
|
||||
// Destroy : unbind from the cropping and scaling interface
|
||||
//
|
||||
// Informs the server that the client will not be using this
|
||||
// protocol object anymore. This does not affect any other objects,
|
||||
// wp_viewport objects included.
|
||||
func (i *WpViewporter) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetViewport : extend surface interface for crop and scale
|
||||
//
|
||||
// Instantiate an interface extension for the given wl_surface to
|
||||
// crop and scale its content. If the given wl_surface already has
|
||||
// a wp_viewport object associated, the viewport_exists
|
||||
// protocol error is raised.
|
||||
//
|
||||
// surface: the surface
|
||||
func (i *WpViewporter) GetViewport(surface *client.Surface) (*WpViewport, error) {
|
||||
id := NewWpViewport(i.Context())
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return id, err
|
||||
}
|
||||
|
||||
type WpViewporterError uint32
|
||||
|
||||
// WpViewporterError :
|
||||
const (
|
||||
// WpViewporterErrorViewportExists : the surface already has a viewport object associated
|
||||
WpViewporterErrorViewportExists WpViewporterError = 0
|
||||
)
|
||||
|
||||
func (e WpViewporterError) Name() string {
|
||||
switch e {
|
||||
case WpViewporterErrorViewportExists:
|
||||
return "viewport_exists"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e WpViewporterError) Value() string {
|
||||
switch e {
|
||||
case WpViewporterErrorViewportExists:
|
||||
return "0"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e WpViewporterError) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
// WpViewportInterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const WpViewportInterfaceName = "wp_viewport"
|
||||
|
||||
// WpViewport : crop and scale interface to a wl_surface
|
||||
//
|
||||
// An additional interface to a wl_surface object, which allows the
|
||||
// client to specify the cropping and scaling of the surface
|
||||
// contents.
|
||||
//
|
||||
// This interface works with two concepts: the source rectangle (src_x,
|
||||
// src_y, src_width, src_height), and the destination size (dst_width,
|
||||
// dst_height). The contents of the source rectangle are scaled to the
|
||||
// destination size, and content outside the source rectangle is ignored.
|
||||
// This state is double-buffered, see wl_surface.commit.
|
||||
//
|
||||
// The two parts of crop and scale state are independent: the source
|
||||
// rectangle, and the destination size. Initially both are unset, that
|
||||
// is, no scaling is applied. The whole of the current wl_buffer is
|
||||
// used as the source, and the surface size is as defined in
|
||||
// wl_surface.attach.
|
||||
//
|
||||
// If the destination size is set, it causes the surface size to become
|
||||
// dst_width, dst_height. The source (rectangle) is scaled to exactly
|
||||
// this size. This overrides whatever the attached wl_buffer size is,
|
||||
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
|
||||
// has no content and therefore no size. Otherwise, the size is always
|
||||
// at least 1x1 in surface local coordinates.
|
||||
//
|
||||
// If the source rectangle is set, it defines what area of the wl_buffer is
|
||||
// taken as the source. If the source rectangle is set and the destination
|
||||
// size is not set, then src_width and src_height must be integers, and the
|
||||
// surface size becomes the source rectangle size. This results in cropping
|
||||
// without scaling. If src_width or src_height are not integers and
|
||||
// destination size is not set, the bad_size protocol error is raised when
|
||||
// the surface state is applied.
|
||||
//
|
||||
// The coordinate transformations from buffer pixel coordinates up to
|
||||
// the surface-local coordinates happen in the following order:
|
||||
// 1. buffer_transform (wl_surface.set_buffer_transform)
|
||||
// 2. buffer_scale (wl_surface.set_buffer_scale)
|
||||
// 3. crop and scale (wp_viewport.set*)
|
||||
// This means, that the source rectangle coordinates of crop and scale
|
||||
// are given in the coordinates after the buffer transform and scale,
|
||||
// i.e. in the coordinates that would be the surface-local coordinates
|
||||
// if the crop and scale was not applied.
|
||||
//
|
||||
// If src_x or src_y are negative, the bad_value protocol error is raised.
|
||||
// Otherwise, if the source rectangle is partially or completely outside of
|
||||
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
|
||||
// when the surface state is applied. A NULL wl_buffer does not raise the
|
||||
// out_of_buffer error.
|
||||
//
|
||||
// If the wl_surface associated with the wp_viewport is destroyed,
|
||||
// all wp_viewport requests except 'destroy' raise the protocol error
|
||||
// no_surface.
|
||||
//
|
||||
// If the wp_viewport object is destroyed, the crop and scale
|
||||
// state is removed from the wl_surface. The change will be applied
|
||||
// on the next wl_surface.commit.
|
||||
type WpViewport struct {
|
||||
client.BaseProxy
|
||||
}
|
||||
|
||||
// NewWpViewport : crop and scale interface to a wl_surface
|
||||
//
|
||||
// An additional interface to a wl_surface object, which allows the
|
||||
// client to specify the cropping and scaling of the surface
|
||||
// contents.
|
||||
//
|
||||
// This interface works with two concepts: the source rectangle (src_x,
|
||||
// src_y, src_width, src_height), and the destination size (dst_width,
|
||||
// dst_height). The contents of the source rectangle are scaled to the
|
||||
// destination size, and content outside the source rectangle is ignored.
|
||||
// This state is double-buffered, see wl_surface.commit.
|
||||
//
|
||||
// The two parts of crop and scale state are independent: the source
|
||||
// rectangle, and the destination size. Initially both are unset, that
|
||||
// is, no scaling is applied. The whole of the current wl_buffer is
|
||||
// used as the source, and the surface size is as defined in
|
||||
// wl_surface.attach.
|
||||
//
|
||||
// If the destination size is set, it causes the surface size to become
|
||||
// dst_width, dst_height. The source (rectangle) is scaled to exactly
|
||||
// this size. This overrides whatever the attached wl_buffer size is,
|
||||
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
|
||||
// has no content and therefore no size. Otherwise, the size is always
|
||||
// at least 1x1 in surface local coordinates.
|
||||
//
|
||||
// If the source rectangle is set, it defines what area of the wl_buffer is
|
||||
// taken as the source. If the source rectangle is set and the destination
|
||||
// size is not set, then src_width and src_height must be integers, and the
|
||||
// surface size becomes the source rectangle size. This results in cropping
|
||||
// without scaling. If src_width or src_height are not integers and
|
||||
// destination size is not set, the bad_size protocol error is raised when
|
||||
// the surface state is applied.
|
||||
//
|
||||
// The coordinate transformations from buffer pixel coordinates up to
|
||||
// the surface-local coordinates happen in the following order:
|
||||
// 1. buffer_transform (wl_surface.set_buffer_transform)
|
||||
// 2. buffer_scale (wl_surface.set_buffer_scale)
|
||||
// 3. crop and scale (wp_viewport.set*)
|
||||
// This means, that the source rectangle coordinates of crop and scale
|
||||
// are given in the coordinates after the buffer transform and scale,
|
||||
// i.e. in the coordinates that would be the surface-local coordinates
|
||||
// if the crop and scale was not applied.
|
||||
//
|
||||
// If src_x or src_y are negative, the bad_value protocol error is raised.
|
||||
// Otherwise, if the source rectangle is partially or completely outside of
|
||||
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
|
||||
// when the surface state is applied. A NULL wl_buffer does not raise the
|
||||
// out_of_buffer error.
|
||||
//
|
||||
// If the wl_surface associated with the wp_viewport is destroyed,
|
||||
// all wp_viewport requests except 'destroy' raise the protocol error
|
||||
// no_surface.
|
||||
//
|
||||
// If the wp_viewport object is destroyed, the crop and scale
|
||||
// state is removed from the wl_surface. The change will be applied
|
||||
// on the next wl_surface.commit.
|
||||
func NewWpViewport(ctx *client.Context) *WpViewport {
|
||||
wpViewport := &WpViewport{}
|
||||
ctx.Register(wpViewport)
|
||||
return wpViewport
|
||||
}
|
||||
|
||||
// Destroy : remove scaling and cropping from the surface
|
||||
//
|
||||
// The associated wl_surface's crop and scale state is removed.
|
||||
// The change is applied on the next wl_surface.commit.
|
||||
func (i *WpViewport) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetSource : set the source rectangle for cropping
|
||||
//
|
||||
// Set the source rectangle of the associated wl_surface. See
|
||||
// wp_viewport for the description, and relation to the wl_buffer
|
||||
// size.
|
||||
//
|
||||
// If all of x, y, width and height are -1.0, the source rectangle is
|
||||
// unset instead. Any other set of values where width or height are zero
|
||||
// or negative, or x or y are negative, raise the bad_value protocol
|
||||
// error.
|
||||
//
|
||||
// The crop and scale state is double-buffered, see wl_surface.commit.
|
||||
//
|
||||
// x: source rectangle x
|
||||
// y: source rectangle y
|
||||
// width: source rectangle width
|
||||
// height: source rectangle height
|
||||
func (i *WpViewport) SetSource(x, y, width, height float64) error {
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8 + 4 + 4 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutFixed(_reqBuf[l:l+4], x)
|
||||
l += 4
|
||||
client.PutFixed(_reqBuf[l:l+4], y)
|
||||
l += 4
|
||||
client.PutFixed(_reqBuf[l:l+4], width)
|
||||
l += 4
|
||||
client.PutFixed(_reqBuf[l:l+4], height)
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDestination : set the surface size for scaling
|
||||
//
|
||||
// Set the destination size of the associated wl_surface. See
|
||||
// wp_viewport for the description, and relation to the wl_buffer
|
||||
// size.
|
||||
//
|
||||
// If width is -1 and height is -1, the destination size is unset
|
||||
// instead. Any other pair of values for width and height that
|
||||
// contains zero or negative values raises the bad_value protocol
|
||||
// error.
|
||||
//
|
||||
// The crop and scale state is double-buffered, see wl_surface.commit.
|
||||
//
|
||||
// width: surface width
|
||||
// height: surface height
|
||||
func (i *WpViewport) SetDestination(width, height int32) error {
|
||||
const opcode = 2
|
||||
const _reqBufLen = 8 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(width))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type WpViewportError uint32
|
||||
|
||||
// WpViewportError :
|
||||
const (
|
||||
// WpViewportErrorBadValue : negative or zero values in width or height
|
||||
WpViewportErrorBadValue WpViewportError = 0
|
||||
// WpViewportErrorBadSize : destination size is not integer
|
||||
WpViewportErrorBadSize WpViewportError = 1
|
||||
// WpViewportErrorOutOfBuffer : source rectangle extends outside of the content area
|
||||
WpViewportErrorOutOfBuffer WpViewportError = 2
|
||||
// WpViewportErrorNoSurface : the wl_surface was destroyed
|
||||
WpViewportErrorNoSurface WpViewportError = 3
|
||||
)
|
||||
|
||||
func (e WpViewportError) Name() string {
|
||||
switch e {
|
||||
case WpViewportErrorBadValue:
|
||||
return "bad_value"
|
||||
case WpViewportErrorBadSize:
|
||||
return "bad_size"
|
||||
case WpViewportErrorOutOfBuffer:
|
||||
return "out_of_buffer"
|
||||
case WpViewportErrorNoSurface:
|
||||
return "no_surface"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e WpViewportError) Value() string {
|
||||
switch e {
|
||||
case WpViewportErrorBadValue:
|
||||
return "0"
|
||||
case WpViewportErrorBadSize:
|
||||
return "1"
|
||||
case WpViewportErrorOutOfBuffer:
|
||||
return "2"
|
||||
case WpViewportErrorNoSurface:
|
||||
return "3"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e WpViewportError) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="keyboard_shortcuts_inhibit_unstable_v1">
|
||||
|
||||
<copyright>
|
||||
Copyright © 2017 Red Hat Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="Protocol for inhibiting the compositor keyboard shortcuts">
|
||||
This protocol specifies a way for a client to request the compositor
|
||||
to ignore its own keyboard shortcuts for a given seat, so that all
|
||||
key events from that seat get forwarded to a surface.
|
||||
|
||||
Warning! The protocol described in this file is experimental and
|
||||
backward incompatible changes may be made. Backward compatible
|
||||
changes may be added together with the corresponding interface
|
||||
version bump.
|
||||
Backward incompatible changes are done by bumping the version
|
||||
number in the protocol and interface names and resetting the
|
||||
interface version. Once the protocol is to be declared stable,
|
||||
the 'z' prefix and the version number in the protocol and
|
||||
interface names are removed and the interface version number is
|
||||
reset.
|
||||
</description>
|
||||
|
||||
<interface name="zwp_keyboard_shortcuts_inhibit_manager_v1" version="1">
|
||||
<description summary="context object for keyboard grab_manager">
|
||||
A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the keyboard shortcuts inhibitor object">
|
||||
Destroy the keyboard shortcuts inhibitor manager.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="inhibit_shortcuts">
|
||||
<description summary="create a new keyboard shortcuts inhibitor object">
|
||||
Create a new keyboard shortcuts inhibitor object associated with
|
||||
the given surface for the given seat.
|
||||
|
||||
If shortcuts are already inhibited for the specified seat and surface,
|
||||
a protocol error "already_inhibited" is raised by the compositor.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zwp_keyboard_shortcuts_inhibitor_v1"/>
|
||||
<arg name="surface" type="object" interface="wl_surface"
|
||||
summary="the surface that inhibits the keyboard shortcuts behavior"/>
|
||||
<arg name="seat" type="object" interface="wl_seat"
|
||||
summary="the wl_seat for which keyboard shortcuts should be disabled"/>
|
||||
</request>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="already_inhibited"
|
||||
value="0"
|
||||
summary="the shortcuts are already inhibited for this surface"/>
|
||||
</enum>
|
||||
</interface>
|
||||
|
||||
<interface name="zwp_keyboard_shortcuts_inhibitor_v1" version="1">
|
||||
<description summary="context object for keyboard shortcuts inhibitor">
|
||||
A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||
its own keyboard shortcuts when the associated surface has keyboard
|
||||
focus. As a result, when the surface has keyboard focus on the given
|
||||
seat, it will receive all key events originating from the specified
|
||||
seat, even those which would normally be caught by the compositor for
|
||||
its own shortcuts.
|
||||
|
||||
The Wayland compositor is however under no obligation to disable
|
||||
all of its shortcuts, and may keep some special key combo for its own
|
||||
use, including but not limited to one allowing the user to forcibly
|
||||
restore normal keyboard events routing in the case of an unwilling
|
||||
client. The compositor may also use the same key combo to reactivate
|
||||
an existing shortcut inhibitor that was previously deactivated on
|
||||
user request.
|
||||
|
||||
When the compositor restores its own keyboard shortcuts, an
|
||||
"inactive" event is emitted to notify the client that the keyboard
|
||||
shortcuts inhibitor is not effectively active for the surface and
|
||||
seat any more, and the client should not expect to receive all
|
||||
keyboard events.
|
||||
|
||||
When the keyboard shortcuts inhibitor is inactive, the client has
|
||||
no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||
|
||||
The user can chose to re-enable a previously deactivated keyboard
|
||||
shortcuts inhibitor using any mechanism the compositor may offer,
|
||||
in which case the compositor will send an "active" event to notify
|
||||
the client.
|
||||
|
||||
If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||
focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||
compositor will restore its own keyboard shortcuts but no "inactive"
|
||||
event is emitted in this case.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the keyboard shortcuts inhibitor object">
|
||||
Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="active">
|
||||
<description summary="shortcuts are inhibited">
|
||||
This event indicates that the shortcut inhibitor is active.
|
||||
|
||||
The compositor sends this event every time compositor shortcuts
|
||||
are inhibited on behalf of the surface. When active, the client
|
||||
may receive input events normally reserved by the compositor
|
||||
(see zwp_keyboard_shortcuts_inhibitor_v1).
|
||||
|
||||
This occurs typically when the initial request "inhibit_shortcuts"
|
||||
first becomes active or when the user instructs the compositor to
|
||||
re-enable and existing shortcuts inhibitor using any mechanism
|
||||
offered by the compositor.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="inactive">
|
||||
<description summary="shortcuts are restored">
|
||||
This event indicates that the shortcuts inhibitor is inactive,
|
||||
normal shortcuts processing is restored by the compositor.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
407
core/internal/proto/xml/wlr-layer-shell-unstable-v1.xml
Normal file
407
core/internal/proto/xml/wlr-layer-shell-unstable-v1.xml
Normal file
@@ -0,0 +1,407 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="wlr_layer_shell_unstable_v1">
|
||||
<copyright>
|
||||
Copyright © 2017 Drew DeVault
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this
|
||||
software and its documentation for any purpose is hereby granted
|
||||
without fee, provided that the above copyright notice appear in
|
||||
all copies and that both that copyright notice and this permission
|
||||
notice appear in supporting documentation, and that the name of
|
||||
the copyright holders not be used in advertising or publicity
|
||||
pertaining to distribution of the software without specific,
|
||||
written prior permission. The copyright holders make no
|
||||
representations about the suitability of this software for any
|
||||
purpose. It is provided "as is" without express or implied
|
||||
warranty.
|
||||
|
||||
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="zwlr_layer_shell_v1" version="5">
|
||||
<description summary="create surfaces that are layers of the desktop">
|
||||
Clients can use this interface to assign the surface_layer role to
|
||||
wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||
rendered with a defined z-depth respective to each other. They may also be
|
||||
anchored to the edges and corners of a screen and specify input handling
|
||||
semantics. This interface should be suitable for the implementation of
|
||||
many desktop shell components, and a broad number of other applications
|
||||
that interact with the desktop.
|
||||
</description>
|
||||
|
||||
<request name="get_layer_surface">
|
||||
<description summary="create a layer_surface from a surface">
|
||||
Create a layer surface for an existing surface. This assigns the role of
|
||||
layer_surface, or raises a protocol error if another role is already
|
||||
assigned.
|
||||
|
||||
Creating a layer surface from a wl_surface which has a buffer attached
|
||||
or committed is a client error, and any attempts by a client to attach
|
||||
or manipulate a buffer prior to the first layer_surface.configure call
|
||||
must also be treated as errors.
|
||||
|
||||
After creating a layer_surface object and setting it up, the client
|
||||
must perform an initial commit without any buffer attached.
|
||||
The compositor will reply with a layer_surface.configure event.
|
||||
The client must acknowledge it and is then allowed to attach a buffer
|
||||
to map the surface.
|
||||
|
||||
You may pass NULL for output to allow the compositor to decide which
|
||||
output to use. Generally this will be the one that the user most
|
||||
recently interacted with.
|
||||
|
||||
Clients can specify a namespace that defines the purpose of the layer
|
||||
surface.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
|
||||
<arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
|
||||
<arg name="namespace" type="string" summary="namespace for the layer surface"/>
|
||||
</request>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="role" value="0" summary="wl_surface has another role"/>
|
||||
<entry name="invalid_layer" value="1" summary="layer value is invalid"/>
|
||||
<entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
|
||||
</enum>
|
||||
|
||||
<enum name="layer">
|
||||
<description summary="available layers for surfaces">
|
||||
These values indicate which layers a surface can be rendered in. They
|
||||
are ordered by z depth, bottom-most first. Traditional shell surfaces
|
||||
will typically be rendered between the bottom and top layers.
|
||||
Fullscreen shell surfaces are typically rendered at the top layer.
|
||||
Multiple surfaces can share a single layer, and ordering within a
|
||||
single layer is undefined.
|
||||
</description>
|
||||
|
||||
<entry name="background" value="0"/>
|
||||
<entry name="bottom" value="1"/>
|
||||
<entry name="top" value="2"/>
|
||||
<entry name="overlay" value="3"/>
|
||||
</enum>
|
||||
|
||||
<!-- Version 3 additions -->
|
||||
|
||||
<request name="destroy" type="destructor" since="3">
|
||||
<description summary="destroy the layer_shell object">
|
||||
This request indicates that the client will not use the layer_shell
|
||||
object any more. Objects that have been created through this instance
|
||||
are not affected.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zwlr_layer_surface_v1" version="5">
|
||||
<description summary="layer metadata interface">
|
||||
An interface that may be implemented by a wl_surface, for surfaces that
|
||||
are designed to be rendered as a layer of a stacked desktop-like
|
||||
environment.
|
||||
|
||||
Layer surface state (layer, size, anchor, exclusive zone,
|
||||
margin, interactivity) is double-buffered, and will be applied at the
|
||||
time wl_surface.commit of the corresponding wl_surface is called.
|
||||
|
||||
Attaching a null buffer to a layer surface unmaps it.
|
||||
|
||||
Unmapping a layer_surface means that the surface cannot be shown by the
|
||||
compositor until it is explicitly mapped again. The layer_surface
|
||||
returns to the state it had right after layer_shell.get_layer_surface.
|
||||
The client can re-map the surface by performing a commit without any
|
||||
buffer attached, waiting for a configure event and handling it as usual.
|
||||
</description>
|
||||
|
||||
<request name="set_size">
|
||||
<description summary="sets the size of the surface">
|
||||
Sets the size of the surface in surface-local coordinates. The
|
||||
compositor will display the surface centered with respect to its
|
||||
anchors.
|
||||
|
||||
If you pass 0 for either value, the compositor will assign it and
|
||||
inform you of the assignment in the configure event. You must set your
|
||||
anchor to opposite edges in the dimensions you omit; not doing so is a
|
||||
protocol error. Both values are 0 by default.
|
||||
|
||||
Size is double-buffered, see wl_surface.commit.
|
||||
</description>
|
||||
<arg name="width" type="uint"/>
|
||||
<arg name="height" type="uint"/>
|
||||
</request>
|
||||
|
||||
<request name="set_anchor">
|
||||
<description summary="configures the anchor point of the surface">
|
||||
Requests that the compositor anchor the surface to the specified edges
|
||||
and corners. If two orthogonal edges are specified (e.g. 'top' and
|
||||
'left'), then the anchor point will be the intersection of the edges
|
||||
(e.g. the top left corner of the output); otherwise the anchor point
|
||||
will be centered on that edge, or in the center if none is specified.
|
||||
|
||||
Anchor is double-buffered, see wl_surface.commit.
|
||||
</description>
|
||||
<arg name="anchor" type="uint" enum="anchor"/>
|
||||
</request>
|
||||
|
||||
<request name="set_exclusive_zone">
|
||||
<description summary="configures the exclusive geometry of this surface">
|
||||
Requests that the compositor avoids occluding an area with other
|
||||
surfaces. The compositor's use of this information is
|
||||
implementation-dependent - do not assume that this region will not
|
||||
actually be occluded.
|
||||
|
||||
A positive value is only meaningful if the surface is anchored to one
|
||||
edge or an edge and both perpendicular edges. If the surface is not
|
||||
anchored, anchored to only two perpendicular edges (a corner), anchored
|
||||
to only two parallel edges or anchored to all edges, a positive value
|
||||
will be treated the same as zero.
|
||||
|
||||
A positive zone is the distance from the edge in surface-local
|
||||
coordinates to consider exclusive.
|
||||
|
||||
Surfaces that do not wish to have an exclusive zone may instead specify
|
||||
how they should interact with surfaces that do. If set to zero, the
|
||||
surface indicates that it would like to be moved to avoid occluding
|
||||
surfaces with a positive exclusive zone. If set to -1, the surface
|
||||
indicates that it would not like to be moved to accommodate for other
|
||||
surfaces, and the compositor should extend it all the way to the edges
|
||||
it is anchored to.
|
||||
|
||||
For example, a panel might set its exclusive zone to 10, so that
|
||||
maximized shell surfaces are not shown on top of it. A notification
|
||||
might set its exclusive zone to 0, so that it is moved to avoid
|
||||
occluding the panel, but shell surfaces are shown underneath it. A
|
||||
wallpaper or lock screen might set their exclusive zone to -1, so that
|
||||
they stretch below or over the panel.
|
||||
|
||||
The default value is 0.
|
||||
|
||||
Exclusive zone is double-buffered, see wl_surface.commit.
|
||||
</description>
|
||||
<arg name="zone" type="int"/>
|
||||
</request>
|
||||
|
||||
<request name="set_margin">
|
||||
<description summary="sets a margin from the anchor point">
|
||||
Requests that the surface be placed some distance away from the anchor
|
||||
point on the output, in surface-local coordinates. Setting this value
|
||||
for edges you are not anchored to has no effect.
|
||||
|
||||
The exclusive zone includes the margin.
|
||||
|
||||
Margin is double-buffered, see wl_surface.commit.
|
||||
</description>
|
||||
<arg name="top" type="int"/>
|
||||
<arg name="right" type="int"/>
|
||||
<arg name="bottom" type="int"/>
|
||||
<arg name="left" type="int"/>
|
||||
</request>
|
||||
|
||||
<enum name="keyboard_interactivity">
|
||||
<description summary="types of keyboard interaction possible for a layer shell surface">
|
||||
Types of keyboard interaction possible for layer shell surfaces. The
|
||||
rationale for this is twofold: (1) some applications are not interested
|
||||
in keyboard events and not allowing them to be focused can improve the
|
||||
desktop experience; (2) some applications will want to take exclusive
|
||||
keyboard focus.
|
||||
</description>
|
||||
|
||||
<entry name="none" value="0">
|
||||
<description summary="no keyboard focus is possible">
|
||||
This value indicates that this surface is not interested in keyboard
|
||||
events and the compositor should never assign it the keyboard focus.
|
||||
|
||||
This is the default value, set for newly created layer shell surfaces.
|
||||
|
||||
This is useful for e.g. desktop widgets that display information or
|
||||
only have interaction with non-keyboard input devices.
|
||||
</description>
|
||||
</entry>
|
||||
<entry name="exclusive" value="1">
|
||||
<description summary="request exclusive keyboard focus">
|
||||
Request exclusive keyboard focus if this surface is above the shell surface layer.
|
||||
|
||||
For the top and overlay layers, the seat will always give
|
||||
exclusive keyboard focus to the top-most layer which has keyboard
|
||||
interactivity set to exclusive. If this layer contains multiple
|
||||
surfaces with keyboard interactivity set to exclusive, the compositor
|
||||
determines the one receiving keyboard events in an implementation-
|
||||
defined manner. In this case, no guarantee is made when this surface
|
||||
will receive keyboard focus (if ever).
|
||||
|
||||
For the bottom and background layers, the compositor is allowed to use
|
||||
normal focus semantics.
|
||||
|
||||
This setting is mainly intended for applications that need to ensure
|
||||
they receive all keyboard events, such as a lock screen or a password
|
||||
prompt.
|
||||
</description>
|
||||
</entry>
|
||||
<entry name="on_demand" value="2" since="4">
|
||||
<description summary="request regular keyboard focus semantics">
|
||||
This requests the compositor to allow this surface to be focused and
|
||||
unfocused by the user in an implementation-defined manner. The user
|
||||
should be able to unfocus this surface even regardless of the layer
|
||||
it is on.
|
||||
|
||||
Typically, the compositor will want to use its normal mechanism to
|
||||
manage keyboard focus between layer shell surfaces with this setting
|
||||
and regular toplevels on the desktop layer (e.g. click to focus).
|
||||
Nevertheless, it is possible for a compositor to require a special
|
||||
interaction to focus or unfocus layer shell surfaces (e.g. requiring
|
||||
a click even if focus follows the mouse normally, or providing a
|
||||
keybinding to switch focus between layers).
|
||||
|
||||
This setting is mainly intended for desktop shell components (e.g.
|
||||
panels) that allow keyboard interaction. Using this option can allow
|
||||
implementing a desktop shell that can be fully usable without the
|
||||
mouse.
|
||||
</description>
|
||||
</entry>
|
||||
</enum>
|
||||
|
||||
<request name="set_keyboard_interactivity">
|
||||
<description summary="requests keyboard events">
|
||||
Set how keyboard events are delivered to this surface. By default,
|
||||
layer shell surfaces do not receive keyboard events; this request can
|
||||
be used to change this.
|
||||
|
||||
This setting is inherited by child surfaces set by the get_popup
|
||||
request.
|
||||
|
||||
Layer surfaces receive pointer, touch, and tablet events normally. If
|
||||
you do not want to receive them, set the input region on your surface
|
||||
to an empty region.
|
||||
|
||||
Keyboard interactivity is double-buffered, see wl_surface.commit.
|
||||
</description>
|
||||
<arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
|
||||
</request>
|
||||
|
||||
<request name="get_popup">
|
||||
<description summary="assign this layer_surface as an xdg_popup parent">
|
||||
This assigns an xdg_popup's parent to this layer_surface. This popup
|
||||
should have been created via xdg_surface::get_popup with the parent set
|
||||
to NULL, and this request must be invoked before committing the popup's
|
||||
initial state.
|
||||
|
||||
See the documentation of xdg_popup for more details about what an
|
||||
xdg_popup is and how it is used.
|
||||
</description>
|
||||
<arg name="popup" type="object" interface="xdg_popup"/>
|
||||
</request>
|
||||
|
||||
<request name="ack_configure">
|
||||
<description summary="ack a configure event">
|
||||
When a configure event is received, if a client commits the
|
||||
surface in response to the configure event, then the client
|
||||
must make an ack_configure request sometime before the commit
|
||||
request, passing along the serial of the configure event.
|
||||
|
||||
If the client receives multiple configure events before it
|
||||
can respond to one, it only has to ack the last configure event.
|
||||
|
||||
A client is not required to commit immediately after sending
|
||||
an ack_configure request - it may even ack_configure several times
|
||||
before its next surface commit.
|
||||
|
||||
A client may send multiple ack_configure requests before committing, but
|
||||
only the last request sent before a commit indicates which configure
|
||||
event the client really is responding to.
|
||||
</description>
|
||||
<arg name="serial" type="uint" summary="the serial from the configure event"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the layer_surface">
|
||||
This request destroys the layer surface.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="configure">
|
||||
<description summary="suggest a surface change">
|
||||
The configure event asks the client to resize its surface.
|
||||
|
||||
Clients should arrange their surface for the new states, and then send
|
||||
an ack_configure request with the serial sent in this configure event at
|
||||
some point before committing the new surface.
|
||||
|
||||
The client is free to dismiss all but the last configure event it
|
||||
received.
|
||||
|
||||
The width and height arguments specify the size of the window in
|
||||
surface-local coordinates.
|
||||
|
||||
The size is a hint, in the sense that the client is free to ignore it if
|
||||
it doesn't resize, pick a smaller size (to satisfy aspect ratio or
|
||||
resize in steps of NxM pixels). If the client picks a smaller size and
|
||||
is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
|
||||
surface will be centered on this axis.
|
||||
|
||||
If the width or height arguments are zero, it means the client should
|
||||
decide its own window dimension.
|
||||
</description>
|
||||
<arg name="serial" type="uint"/>
|
||||
<arg name="width" type="uint"/>
|
||||
<arg name="height" type="uint"/>
|
||||
</event>
|
||||
|
||||
<event name="closed">
|
||||
<description summary="surface should be closed">
|
||||
The closed event is sent by the compositor when the surface will no
|
||||
longer be shown. The output may have been destroyed or the user may
|
||||
have asked for it to be removed. Further changes to the surface will be
|
||||
ignored. The client should destroy the resource after receiving this
|
||||
event, and create a new surface if they so choose.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
|
||||
<entry name="invalid_size" value="1" summary="size is invalid"/>
|
||||
<entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
|
||||
<entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
|
||||
<entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
|
||||
</enum>
|
||||
|
||||
<enum name="anchor" bitfield="true">
|
||||
<entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
|
||||
<entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
|
||||
<entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
|
||||
<entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
|
||||
</enum>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
|
||||
<request name="set_layer" since="2">
|
||||
<description summary="change the layer of the surface">
|
||||
Change the layer that the surface is rendered on.
|
||||
|
||||
Layer is double-buffered, see wl_surface.commit.
|
||||
</description>
|
||||
<arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
|
||||
</request>
|
||||
|
||||
<!-- Version 5 additions -->
|
||||
|
||||
<request name="set_exclusive_edge" since="5">
|
||||
<description summary="set the edge the exclusive zone will be applied to">
|
||||
Requests an edge for the exclusive zone to apply. The exclusive
|
||||
edge will be automatically deduced from anchor points when possible,
|
||||
but when the surface is anchored to a corner, it will be necessary
|
||||
to set it explicitly to disambiguate, as it is not possible to deduce
|
||||
which one of the two corner edges should be used.
|
||||
|
||||
The edge must be one the surface is anchored to, otherwise the
|
||||
invalid_exclusive_edge protocol error will be raised.
|
||||
</description>
|
||||
<arg name="edge" type="uint" enum="anchor"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
234
core/internal/proto/xml/wlr-screencopy-unstable-v1.xml
Normal file
234
core/internal/proto/xml/wlr-screencopy-unstable-v1.xml
Normal file
@@ -0,0 +1,234 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="wlr_screencopy_unstable_v1">
|
||||
<copyright>
|
||||
Copyright © 2018 Simon Ser
|
||||
Copyright © 2019 Andri Yngvason
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="screen content capturing on client buffers">
|
||||
This protocol allows clients to ask the compositor to copy part of the
|
||||
screen content to a client buffer.
|
||||
|
||||
Warning! The protocol described in this file is experimental and
|
||||
backward incompatible changes may be made. Backward compatible changes
|
||||
may be added together with the corresponding interface version bump.
|
||||
Backward incompatible changes are done by bumping the version number in
|
||||
the protocol and interface names and resetting the interface version.
|
||||
Once the protocol is to be declared stable, the 'z' prefix and the
|
||||
version number in the protocol and interface names are removed and the
|
||||
interface version number is reset.
|
||||
|
||||
Note! This protocol is deprecated and not intended for production use.
|
||||
The ext-image-copy-capture-v1 protocol should be used instead.
|
||||
</description>
|
||||
|
||||
<interface name="zwlr_screencopy_manager_v1" version="3">
|
||||
<description summary="manager to inform clients and begin capturing">
|
||||
This object is a manager which offers requests to start capturing from a
|
||||
source.
|
||||
</description>
|
||||
|
||||
<request name="capture_output">
|
||||
<description summary="capture an output">
|
||||
Capture the next frame of an entire output.
|
||||
</description>
|
||||
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||
<arg name="overlay_cursor" type="int"
|
||||
summary="composite cursor onto the frame"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</request>
|
||||
|
||||
<request name="capture_output_region">
|
||||
<description summary="capture an output's region">
|
||||
Capture the next frame of an output's region.
|
||||
|
||||
The region is given in output logical coordinates, see
|
||||
xdg_output.logical_size. The region will be clipped to the output's
|
||||
extents.
|
||||
</description>
|
||||
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||
<arg name="overlay_cursor" type="int"
|
||||
summary="composite cursor onto the frame"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
<arg name="x" type="int"/>
|
||||
<arg name="y" type="int"/>
|
||||
<arg name="width" type="int"/>
|
||||
<arg name="height" type="int"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the manager">
|
||||
All objects created by the manager will still remain valid, until their
|
||||
appropriate destroy request has been called.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zwlr_screencopy_frame_v1" version="3">
|
||||
<description summary="a frame ready for copy">
|
||||
This object represents a single frame.
|
||||
|
||||
When created, a series of buffer events will be sent, each representing a
|
||||
supported buffer type. The "buffer_done" event is sent afterwards to
|
||||
indicate that all supported buffer types have been enumerated. The client
|
||||
will then be able to send a "copy" request. If the capture is successful,
|
||||
the compositor will send a "flags" event followed by a "ready" event.
|
||||
|
||||
For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||
the "buffer" event is guaranteed to be sent.
|
||||
|
||||
If the capture failed, the "failed" event is sent. This can happen anytime
|
||||
before the "ready" event.
|
||||
|
||||
Once either a "ready" or a "failed" event is received, the client should
|
||||
destroy the frame.
|
||||
</description>
|
||||
|
||||
<event name="buffer">
|
||||
<description summary="wl_shm buffer information">
|
||||
Provides information about wl_shm buffer parameters that need to be
|
||||
used for this frame. This event is sent once after the frame is created
|
||||
if wl_shm buffers are supported.
|
||||
</description>
|
||||
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
|
||||
<arg name="width" type="uint" summary="buffer width"/>
|
||||
<arg name="height" type="uint" summary="buffer height"/>
|
||||
<arg name="stride" type="uint" summary="buffer stride"/>
|
||||
</event>
|
||||
|
||||
<request name="copy">
|
||||
<description summary="copy the frame">
|
||||
Copy the frame to the supplied buffer. The buffer must have the
|
||||
correct size, see zwlr_screencopy_frame_v1.buffer and
|
||||
zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
|
||||
supported format.
|
||||
|
||||
If the frame is successfully copied, "flags" and "ready" events are
|
||||
sent. Otherwise, a "failed" event is sent.
|
||||
</description>
|
||||
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||
</request>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="already_used" value="0"
|
||||
summary="the object has already been used to copy a wl_buffer"/>
|
||||
<entry name="invalid_buffer" value="1"
|
||||
summary="buffer attributes are invalid"/>
|
||||
</enum>
|
||||
|
||||
<enum name="flags" bitfield="true">
|
||||
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
|
||||
</enum>
|
||||
|
||||
<event name="flags">
|
||||
<description summary="frame flags">
|
||||
Provides flags about the frame. This event is sent once before the
|
||||
"ready" event.
|
||||
</description>
|
||||
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
|
||||
</event>
|
||||
|
||||
<event name="ready">
|
||||
<description summary="indicates frame is available for reading">
|
||||
Called as soon as the frame is copied, indicating it is available
|
||||
for reading. This event includes the time at which the presentation took place.
|
||||
|
||||
The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
|
||||
each component being an unsigned 32-bit value. Whole seconds are in
|
||||
tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
|
||||
and the additional fractional part in tv_nsec as nanoseconds. Hence,
|
||||
for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
|
||||
may have an arbitrary offset at start.
|
||||
|
||||
After receiving this event, the client should destroy the object.
|
||||
</description>
|
||||
<arg name="tv_sec_hi" type="uint"
|
||||
summary="high 32 bits of the seconds part of the timestamp"/>
|
||||
<arg name="tv_sec_lo" type="uint"
|
||||
summary="low 32 bits of the seconds part of the timestamp"/>
|
||||
<arg name="tv_nsec" type="uint"
|
||||
summary="nanoseconds part of the timestamp"/>
|
||||
</event>
|
||||
|
||||
<event name="failed">
|
||||
<description summary="frame copy failed">
|
||||
This event indicates that the attempted frame copy has failed.
|
||||
|
||||
After receiving this event, the client should destroy the object.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="delete this object, used or not">
|
||||
Destroys the frame. This request can be sent at any time by the client.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
<request name="copy_with_damage" since="2">
|
||||
<description summary="copy the frame when it's damaged">
|
||||
Same as copy, except it waits until there is damage to copy.
|
||||
</description>
|
||||
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||
</request>
|
||||
|
||||
<event name="damage" since="2">
|
||||
<description summary="carries the coordinates of the damaged region">
|
||||
This event is sent right before the ready event when copy_with_damage is
|
||||
requested. It may be generated multiple times for each copy_with_damage
|
||||
request.
|
||||
|
||||
The arguments describe a box around an area that has changed since the
|
||||
last copy request that was derived from the current screencopy manager
|
||||
instance.
|
||||
|
||||
The union of all regions received between the call to copy_with_damage
|
||||
and a ready event is the total damage since the prior ready event.
|
||||
</description>
|
||||
<arg name="x" type="uint" summary="damaged x coordinates"/>
|
||||
<arg name="y" type="uint" summary="damaged y coordinates"/>
|
||||
<arg name="width" type="uint" summary="current width"/>
|
||||
<arg name="height" type="uint" summary="current height"/>
|
||||
</event>
|
||||
|
||||
<!-- Version 3 additions -->
|
||||
<event name="linux_dmabuf" since="3">
|
||||
<description summary="linux-dmabuf buffer information">
|
||||
Provides information about linux-dmabuf buffer parameters that need to
|
||||
be used for this frame. This event is sent once after the frame is
|
||||
created if linux-dmabuf buffers are supported.
|
||||
</description>
|
||||
<arg name="format" type="uint" summary="fourcc pixel format"/>
|
||||
<arg name="width" type="uint" summary="buffer width"/>
|
||||
<arg name="height" type="uint" summary="buffer height"/>
|
||||
</event>
|
||||
|
||||
<event name="buffer_done" since="3">
|
||||
<description summary="all buffer types reported">
|
||||
This event is sent once after all buffer events have been sent.
|
||||
|
||||
The client should proceed to create a buffer of one of the supported
|
||||
types, and send a "copy" request.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
69
core/internal/screenshot/compositor.go
Normal file
69
core/internal/screenshot/compositor.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type Compositor int
|
||||
|
||||
const (
|
||||
CompositorUnknown Compositor = iota
|
||||
CompositorHyprland
|
||||
)
|
||||
|
||||
func DetectCompositor() Compositor {
|
||||
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||
return CompositorHyprland
|
||||
}
|
||||
return CompositorUnknown
|
||||
}
|
||||
|
||||
type WindowGeometry struct {
|
||||
X int32
|
||||
Y int32
|
||||
Width int32
|
||||
Height int32
|
||||
}
|
||||
|
||||
func GetActiveWindow() (*WindowGeometry, error) {
|
||||
compositor := DetectCompositor()
|
||||
|
||||
switch compositor {
|
||||
case CompositorHyprland:
|
||||
return getHyprlandActiveWindow()
|
||||
default:
|
||||
return nil, fmt.Errorf("window capture requires Hyprland (other compositors not yet supported)")
|
||||
}
|
||||
}
|
||||
|
||||
type hyprlandWindow struct {
|
||||
At [2]int32 `json:"at"`
|
||||
Size [2]int32 `json:"size"`
|
||||
}
|
||||
|
||||
func getHyprlandActiveWindow() (*WindowGeometry, error) {
|
||||
cmd := exec.Command("hyprctl", "-j", "activewindow")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hyprctl activewindow: %w", err)
|
||||
}
|
||||
|
||||
var win hyprlandWindow
|
||||
if err := json.Unmarshal(output, &win); err != nil {
|
||||
return nil, fmt.Errorf("parse activewindow: %w", err)
|
||||
}
|
||||
|
||||
if win.Size[0] <= 0 || win.Size[1] <= 0 {
|
||||
return nil, fmt.Errorf("no active window")
|
||||
}
|
||||
|
||||
return &WindowGeometry{
|
||||
X: win.At[0],
|
||||
Y: win.At[1],
|
||||
Width: win.Size[0],
|
||||
Height: win.Size[1],
|
||||
}, nil
|
||||
}
|
||||
197
core/internal/screenshot/encode.go
Normal file
197
core/internal/screenshot/encode.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func BufferToImage(buf *ShmBuffer) *image.RGBA {
|
||||
return BufferToImageWithFormat(buf, uint32(FormatARGB8888))
|
||||
}
|
||||
|
||||
func BufferToImageWithFormat(buf *ShmBuffer, format uint32) *image.RGBA {
|
||||
img := image.NewRGBA(image.Rect(0, 0, buf.Width, buf.Height))
|
||||
data := buf.Data()
|
||||
|
||||
swapRB := format == uint32(FormatARGB8888) || format == uint32(FormatXRGB8888) || format == 0
|
||||
|
||||
for y := 0; y < buf.Height; y++ {
|
||||
srcOff := y * buf.Stride
|
||||
dstOff := y * img.Stride
|
||||
for x := 0; x < buf.Width; x++ {
|
||||
si := srcOff + x*4
|
||||
di := dstOff + x*4
|
||||
if si+3 >= len(data) || di+3 >= len(img.Pix) {
|
||||
continue
|
||||
}
|
||||
if swapRB {
|
||||
img.Pix[di+0] = data[si+2]
|
||||
img.Pix[di+1] = data[si+1]
|
||||
img.Pix[di+2] = data[si+0]
|
||||
} else {
|
||||
img.Pix[di+0] = data[si+0]
|
||||
img.Pix[di+1] = data[si+1]
|
||||
img.Pix[di+2] = data[si+2]
|
||||
}
|
||||
img.Pix[di+3] = 255
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func EncodePNG(w io.Writer, img image.Image) error {
|
||||
enc := png.Encoder{CompressionLevel: png.BestSpeed}
|
||||
return enc.Encode(w, img)
|
||||
}
|
||||
|
||||
func EncodeJPEG(w io.Writer, img image.Image, quality int) error {
|
||||
return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
|
||||
}
|
||||
|
||||
func EncodePPM(w io.Writer, img *image.RGBA) error {
|
||||
bw := bufio.NewWriter(w)
|
||||
bounds := img.Bounds()
|
||||
if _, err := fmt.Fprintf(bw, "P6\n%d %d\n255\n", bounds.Dx(), bounds.Dy()); err != nil {
|
||||
return err
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
off := (y-bounds.Min.Y)*img.Stride + (x-bounds.Min.X)*4
|
||||
if err := bw.WriteByte(img.Pix[off+0]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bw.WriteByte(img.Pix[off+1]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bw.WriteByte(img.Pix[off+2]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
func GenerateFilename(format Format) string {
|
||||
t := time.Now()
|
||||
ext := "png"
|
||||
switch format {
|
||||
case FormatJPEG:
|
||||
ext = "jpg"
|
||||
case FormatPPM:
|
||||
ext = "ppm"
|
||||
}
|
||||
return fmt.Sprintf("screenshot_%s.%s", t.Format("2006-01-02_15-04-05"), ext)
|
||||
}
|
||||
|
||||
func GetOutputDir() string {
|
||||
if dir := os.Getenv("DMS_SCREENSHOT_DIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
|
||||
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
|
||||
screenshotDir := filepath.Join(xdgPics, "Screenshots")
|
||||
if err := os.MkdirAll(screenshotDir, 0755); err == nil {
|
||||
return screenshotDir
|
||||
}
|
||||
return xdgPics
|
||||
}
|
||||
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
return home
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
func getXDGPicturesDir() string {
|
||||
configDir := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configDir == "" {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
return ""
|
||||
}
|
||||
configDir = filepath.Join(home, ".config")
|
||||
}
|
||||
|
||||
userDirsFile := filepath.Join(configDir, "user-dirs.dirs")
|
||||
data, err := os.ReadFile(userDirsFile)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, line := range splitLines(string(data)) {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
const prefix = "XDG_PICTURES_DIR="
|
||||
if len(line) > len(prefix) && line[:len(prefix)] == prefix {
|
||||
path := line[len(prefix):]
|
||||
path = trimQuotes(path)
|
||||
path = expandHome(path)
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
var lines []string
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\n' {
|
||||
lines = append(lines, s[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(s) {
|
||||
lines = append(lines, s[start:])
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func trimQuotes(s string) string {
|
||||
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func expandHome(path string) string {
|
||||
if len(path) >= 5 && path[:5] == "$HOME" {
|
||||
home := os.Getenv("HOME")
|
||||
return home + path[5:]
|
||||
}
|
||||
if len(path) >= 1 && path[0] == '~' {
|
||||
home := os.Getenv("HOME")
|
||||
return home + path[1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func WriteToFile(buf *ShmBuffer, path string, format Format, quality int) error {
|
||||
return WriteToFileWithFormat(buf, path, format, quality, uint32(FormatARGB8888))
|
||||
}
|
||||
|
||||
func WriteToFileWithFormat(buf *ShmBuffer, path string, format Format, quality int, pixelFormat uint32) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img := BufferToImageWithFormat(buf, pixelFormat)
|
||||
switch format {
|
||||
case FormatJPEG:
|
||||
return EncodeJPEG(f, img, quality)
|
||||
case FormatPPM:
|
||||
return EncodePPM(f, img)
|
||||
default:
|
||||
return EncodePNG(f, img)
|
||||
}
|
||||
}
|
||||
180
core/internal/screenshot/notify.go
Normal file
180
core/internal/screenshot/notify.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyDest = "org.freedesktop.Notifications"
|
||||
notifyPath = "/org/freedesktop/Notifications"
|
||||
notifyInterface = "org.freedesktop.Notifications"
|
||||
)
|
||||
|
||||
type NotifyResult struct {
|
||||
FilePath string
|
||||
Clipboard bool
|
||||
ImageData []byte
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
func SendNotification(result NotifyResult) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
log.Debug("dbus session failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
var actions []string
|
||||
if result.FilePath != "" {
|
||||
actions = []string{"default", "Open"}
|
||||
}
|
||||
|
||||
hints := map[string]dbus.Variant{}
|
||||
if len(result.ImageData) > 0 && result.Width > 0 && result.Height > 0 {
|
||||
rowstride := result.Width * 3
|
||||
hints["image_data"] = dbus.MakeVariant(struct {
|
||||
Width int32
|
||||
Height int32
|
||||
Rowstride int32
|
||||
HasAlpha bool
|
||||
BitsPerSample int32
|
||||
Channels int32
|
||||
Data []byte
|
||||
}{
|
||||
Width: int32(result.Width),
|
||||
Height: int32(result.Height),
|
||||
Rowstride: int32(rowstride),
|
||||
HasAlpha: false,
|
||||
BitsPerSample: 8,
|
||||
Channels: 3,
|
||||
Data: result.ImageData,
|
||||
})
|
||||
} else if result.FilePath != "" {
|
||||
hints["image_path"] = dbus.MakeVariant(result.FilePath)
|
||||
}
|
||||
|
||||
summary := "Screenshot captured"
|
||||
body := ""
|
||||
if result.Clipboard && result.FilePath != "" {
|
||||
body = fmt.Sprintf("Copied to clipboard\n%s", filepath.Base(result.FilePath))
|
||||
} else if result.Clipboard {
|
||||
body = "Copied to clipboard"
|
||||
} else if result.FilePath != "" {
|
||||
body = filepath.Base(result.FilePath)
|
||||
}
|
||||
|
||||
obj := conn.Object(notifyDest, notifyPath)
|
||||
call := obj.Call(
|
||||
notifyInterface+".Notify",
|
||||
0,
|
||||
"DMS",
|
||||
uint32(0),
|
||||
"",
|
||||
summary,
|
||||
body,
|
||||
actions,
|
||||
hints,
|
||||
int32(5000),
|
||||
)
|
||||
|
||||
if call.Err != nil {
|
||||
log.Debug("notify call failed", "err", call.Err)
|
||||
return
|
||||
}
|
||||
|
||||
var notificationID uint32
|
||||
if err := call.Store(¬ificationID); err != nil {
|
||||
log.Debug("failed to get notification id", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(actions) == 0 || result.FilePath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
spawnActionListener(notificationID, result.FilePath)
|
||||
}
|
||||
|
||||
func spawnActionListener(notificationID uint32, filePath string) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Debug("failed to get executable", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, "notify-action", fmt.Sprintf("%d", notificationID), filePath)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
|
||||
func RunNotifyActionListener(args []string) {
|
||||
if len(args) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
notificationID, err := strconv.ParseUint(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := args[1]
|
||||
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(notifyPath),
|
||||
dbus.WithMatchInterface(notifyInterface),
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
signals := make(chan *dbus.Signal, 10)
|
||||
conn.Signal(signals)
|
||||
|
||||
for sig := range signals {
|
||||
switch sig.Name {
|
||||
case notifyInterface + ".ActionInvoked":
|
||||
if len(sig.Body) < 2 {
|
||||
continue
|
||||
}
|
||||
id, ok := sig.Body[0].(uint32)
|
||||
if !ok || id != uint32(notificationID) {
|
||||
continue
|
||||
}
|
||||
openFile(filePath)
|
||||
return
|
||||
|
||||
case notifyInterface + ".NotificationClosed":
|
||||
if len(sig.Body) < 1 {
|
||||
continue
|
||||
}
|
||||
id, ok := sig.Body[0].(uint32)
|
||||
if !ok || id != uint32(notificationID) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func openFile(filePath string) {
|
||||
cmd := exec.Command("xdg-open", filePath)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
cmd.Start()
|
||||
}
|
||||
805
core/internal/screenshot/region.go
Normal file
805
core/internal/screenshot/region.go
Normal file
@@ -0,0 +1,805 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type SelectionState struct {
|
||||
hasSelection bool // There's a selection to display (pre-loaded or user-drawn)
|
||||
dragging bool // User is actively drawing a new selection
|
||||
surface *OutputSurface // Surface where selection was made
|
||||
// Surface-local logical coordinates (from pointer events)
|
||||
anchorX float64
|
||||
anchorY float64
|
||||
currentX float64
|
||||
currentY float64
|
||||
}
|
||||
|
||||
type RenderSlot struct {
|
||||
shm *ShmBuffer
|
||||
pool *client.ShmPool
|
||||
wlBuf *client.Buffer
|
||||
busy bool
|
||||
}
|
||||
|
||||
type OutputSurface struct {
|
||||
output *WaylandOutput
|
||||
wlSurface *client.Surface
|
||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||
viewport *wp_viewporter.WpViewport
|
||||
screenBuf *ShmBuffer
|
||||
screenBufNoCursor *ShmBuffer
|
||||
screenFormat uint32
|
||||
logicalW int
|
||||
logicalH int
|
||||
configured bool
|
||||
yInverted bool
|
||||
|
||||
// Triple-buffered render slots
|
||||
slots [3]*RenderSlot
|
||||
slotsReady bool
|
||||
}
|
||||
|
||||
type PreCapture struct {
|
||||
screenBuf *ShmBuffer
|
||||
screenBufNoCursor *ShmBuffer
|
||||
format uint32
|
||||
yInverted bool
|
||||
}
|
||||
|
||||
type RegionSelector struct {
|
||||
screenshoter *Screenshoter
|
||||
|
||||
display *client.Display
|
||||
registry *client.Registry
|
||||
ctx *client.Context
|
||||
|
||||
compositor *client.Compositor
|
||||
shm *client.Shm
|
||||
seat *client.Seat
|
||||
pointer *client.Pointer
|
||||
keyboard *client.Keyboard
|
||||
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||
viewporter *wp_viewporter.WpViewporter
|
||||
|
||||
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
||||
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
||||
|
||||
outputs map[uint32]*WaylandOutput
|
||||
outputsMu sync.Mutex
|
||||
preCapture map[*WaylandOutput]*PreCapture
|
||||
|
||||
surfaces []*OutputSurface
|
||||
activeSurface *OutputSurface
|
||||
|
||||
// Cursor surface for crosshair
|
||||
cursorSurface *client.Surface
|
||||
cursorBuffer *ShmBuffer
|
||||
cursorWlBuf *client.Buffer
|
||||
cursorPool *client.ShmPool
|
||||
|
||||
selection SelectionState
|
||||
pointerX float64
|
||||
pointerY float64
|
||||
preSelect Region
|
||||
showCapturedCursor bool
|
||||
shiftHeld bool
|
||||
|
||||
running bool
|
||||
cancelled bool
|
||||
result Region
|
||||
|
||||
capturedBuffer *ShmBuffer
|
||||
capturedRegion Region
|
||||
}
|
||||
|
||||
func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
||||
return &RegionSelector{
|
||||
screenshoter: s,
|
||||
outputs: make(map[uint32]*WaylandOutput),
|
||||
preCapture: make(map[*WaylandOutput]*PreCapture),
|
||||
showCapturedCursor: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
||||
r.preSelect = GetLastRegion()
|
||||
|
||||
if err := r.connect(); err != nil {
|
||||
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer r.cleanup()
|
||||
|
||||
if err := r.setupRegistry(); err != nil {
|
||||
return nil, false, fmt.Errorf("registry setup: %w", err)
|
||||
}
|
||||
|
||||
if err := r.roundtrip(); err != nil {
|
||||
return nil, false, fmt.Errorf("roundtrip after registry: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.screencopy == nil:
|
||||
return nil, false, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||
case r.layerShell == nil:
|
||||
return nil, false, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
|
||||
case r.seat == nil:
|
||||
return nil, false, fmt.Errorf("no seat available")
|
||||
case r.compositor == nil:
|
||||
return nil, false, fmt.Errorf("compositor not available")
|
||||
case r.shm == nil:
|
||||
return nil, false, fmt.Errorf("wl_shm not available")
|
||||
case len(r.outputs) == 0:
|
||||
return nil, false, fmt.Errorf("no outputs available")
|
||||
}
|
||||
|
||||
if err := r.roundtrip(); err != nil {
|
||||
return nil, false, fmt.Errorf("roundtrip after protocol check: %w", err)
|
||||
}
|
||||
|
||||
if err := r.preCaptureAllOutputs(); err != nil {
|
||||
return nil, false, fmt.Errorf("pre-capture: %w", err)
|
||||
}
|
||||
|
||||
if err := r.createSurfaces(); err != nil {
|
||||
return nil, false, fmt.Errorf("create surfaces: %w", err)
|
||||
}
|
||||
|
||||
_ = r.createCursor()
|
||||
|
||||
if err := r.roundtrip(); err != nil {
|
||||
return nil, false, fmt.Errorf("roundtrip after surfaces: %w", err)
|
||||
}
|
||||
|
||||
r.running = true
|
||||
for r.running {
|
||||
if err := r.ctx.Dispatch(); err != nil {
|
||||
return nil, false, fmt.Errorf("dispatch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.cancelled || r.capturedBuffer == nil {
|
||||
return nil, r.cancelled, nil
|
||||
}
|
||||
|
||||
yInverted := false
|
||||
var format uint32
|
||||
if r.selection.surface != nil {
|
||||
yInverted = r.selection.surface.yInverted
|
||||
format = r.selection.surface.screenFormat
|
||||
}
|
||||
|
||||
return &CaptureResult{
|
||||
Buffer: r.capturedBuffer,
|
||||
Region: r.result,
|
||||
YInverted: yInverted,
|
||||
Format: format,
|
||||
}, false, nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) connect() error {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.display = display
|
||||
r.ctx = display.Context()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) roundtrip() error {
|
||||
return wlhelpers.Roundtrip(r.display, r.ctx)
|
||||
}
|
||||
|
||||
func (r *RegionSelector) setupRegistry() error {
|
||||
registry, err := r.display.GetRegistry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.registry = registry
|
||||
|
||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||
r.handleGlobal(e)
|
||||
})
|
||||
|
||||
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||
r.outputsMu.Lock()
|
||||
delete(r.outputs, e.Name)
|
||||
r.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) handleGlobal(e client.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case client.CompositorInterfaceName:
|
||||
comp := client.NewCompositor(r.ctx)
|
||||
if err := r.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
|
||||
r.compositor = comp
|
||||
}
|
||||
|
||||
case client.ShmInterfaceName:
|
||||
shm := client.NewShm(r.ctx)
|
||||
if err := r.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||
r.shm = shm
|
||||
}
|
||||
|
||||
case client.SeatInterfaceName:
|
||||
seat := client.NewSeat(r.ctx)
|
||||
if err := r.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
|
||||
r.seat = seat
|
||||
r.setupInput()
|
||||
}
|
||||
|
||||
case client.OutputInterfaceName:
|
||||
output := client.NewOutput(r.ctx)
|
||||
version := e.Version
|
||||
if version > 4 {
|
||||
version = 4
|
||||
}
|
||||
if err := r.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||
r.outputsMu.Lock()
|
||||
r.outputs[e.Name] = &WaylandOutput{
|
||||
wlOutput: output,
|
||||
globalName: e.Name,
|
||||
scale: 1,
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
r.setupOutputHandlers(e.Name, output)
|
||||
}
|
||||
|
||||
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||
ls := wlr_layer_shell.NewZwlrLayerShellV1(r.ctx)
|
||||
version := e.Version
|
||||
if version > 4 {
|
||||
version = 4
|
||||
}
|
||||
if err := r.registry.Bind(e.Name, e.Interface, version, ls); err == nil {
|
||||
r.layerShell = ls
|
||||
}
|
||||
|
||||
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||
sc := wlr_screencopy.NewZwlrScreencopyManagerV1(r.ctx)
|
||||
version := e.Version
|
||||
if version > 3 {
|
||||
version = 3
|
||||
}
|
||||
if err := r.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
|
||||
r.screencopy = sc
|
||||
}
|
||||
|
||||
case wp_viewporter.WpViewporterInterfaceName:
|
||||
vp := wp_viewporter.NewWpViewporter(r.ctx)
|
||||
if err := r.registry.Bind(e.Name, e.Interface, e.Version, vp); err == nil {
|
||||
r.viewporter = vp
|
||||
}
|
||||
|
||||
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
||||
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(r.ctx)
|
||||
if err := r.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||
r.shortcutsInhibitMgr = mgr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) setupOutputHandlers(name uint32, output *client.Output) {
|
||||
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||
r.outputsMu.Lock()
|
||||
if o, ok := r.outputs[name]; ok {
|
||||
o.x = e.X
|
||||
o.y = e.Y
|
||||
o.transform = int32(e.Transform)
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||
return
|
||||
}
|
||||
r.outputsMu.Lock()
|
||||
if o, ok := r.outputs[name]; ok {
|
||||
o.width = e.Width
|
||||
o.height = e.Height
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||
r.outputsMu.Lock()
|
||||
if o, ok := r.outputs[name]; ok {
|
||||
o.scale = e.Factor
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||
r.outputsMu.Lock()
|
||||
if o, ok := r.outputs[name]; ok {
|
||||
o.name = e.Name
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RegionSelector) preCaptureAllOutputs() error {
|
||||
r.outputsMu.Lock()
|
||||
outputs := make([]*WaylandOutput, 0, len(r.outputs))
|
||||
for _, o := range r.outputs {
|
||||
outputs = append(outputs, o)
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
|
||||
pending := len(outputs) * 2
|
||||
done := make(chan struct{}, pending)
|
||||
|
||||
for _, output := range outputs {
|
||||
pc := &PreCapture{}
|
||||
r.preCapture[output] = pc
|
||||
|
||||
r.preCaptureOutput(output, pc, true, func() { done <- struct{}{} })
|
||||
r.preCaptureOutput(output, pc, false, func() { done <- struct{}{} })
|
||||
}
|
||||
|
||||
for i := 0; i < pending; i++ {
|
||||
if err := r.ctx.Dispatch(); err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
i--
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture, withCursor bool, onReady func()) {
|
||||
cursor := int32(0)
|
||||
if withCursor {
|
||||
cursor = 1
|
||||
}
|
||||
|
||||
frame, err := r.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||
if err != nil {
|
||||
log.Error("screencopy capture failed", "err", err)
|
||||
onReady()
|
||||
return
|
||||
}
|
||||
|
||||
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||
buf, err := CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||
if err != nil {
|
||||
log.Error("create screen buffer failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if withCursor {
|
||||
pc.screenBuf = buf
|
||||
pc.format = e.Format
|
||||
} else {
|
||||
pc.screenBufNoCursor = buf
|
||||
}
|
||||
|
||||
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||
if err != nil {
|
||||
log.Error("create shm pool failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), e.Format)
|
||||
if err != nil {
|
||||
log.Error("create wl_buffer failed", "err", err)
|
||||
pool.Destroy()
|
||||
return
|
||||
}
|
||||
|
||||
if err := frame.Copy(wlBuf); err != nil {
|
||||
log.Error("frame copy failed", "err", err)
|
||||
}
|
||||
pool.Destroy()
|
||||
})
|
||||
|
||||
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
|
||||
if withCursor {
|
||||
pc.yInverted = (e.Flags & 1) != 0
|
||||
}
|
||||
})
|
||||
|
||||
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||
frame.Destroy()
|
||||
onReady()
|
||||
})
|
||||
|
||||
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
|
||||
log.Error("screencopy failed")
|
||||
frame.Destroy()
|
||||
onReady()
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RegionSelector) createSurfaces() error {
|
||||
r.outputsMu.Lock()
|
||||
outputs := make([]*WaylandOutput, 0, len(r.outputs))
|
||||
for _, o := range r.outputs {
|
||||
outputs = append(outputs, o)
|
||||
}
|
||||
r.outputsMu.Unlock()
|
||||
|
||||
for _, output := range outputs {
|
||||
os, err := r.createOutputSurface(output)
|
||||
if err != nil {
|
||||
return fmt.Errorf("output %s: %w", output.name, err)
|
||||
}
|
||||
r.surfaces = append(r.surfaces, os)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) createCursor() error {
|
||||
const size = 24
|
||||
const hotspot = size / 2
|
||||
|
||||
surface, err := r.compositor.CreateSurface()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create cursor surface: %w", err)
|
||||
}
|
||||
r.cursorSurface = surface
|
||||
|
||||
buf, err := CreateShmBuffer(size, size, size*4)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create cursor buffer: %w", err)
|
||||
}
|
||||
r.cursorBuffer = buf
|
||||
|
||||
// Draw crosshair
|
||||
data := buf.Data()
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
off := (y*size + x) * 4
|
||||
// Vertical line
|
||||
if x >= hotspot-1 && x <= hotspot && y >= 2 && y < size-2 {
|
||||
data[off+0] = 255 // B
|
||||
data[off+1] = 255 // G
|
||||
data[off+2] = 255 // R
|
||||
data[off+3] = 255 // A
|
||||
continue
|
||||
}
|
||||
// Horizontal line
|
||||
if y >= hotspot-1 && y <= hotspot && x >= 2 && x < size-2 {
|
||||
data[off+0] = 255
|
||||
data[off+1] = 255
|
||||
data[off+2] = 255
|
||||
data[off+3] = 255
|
||||
continue
|
||||
}
|
||||
// Transparent
|
||||
data[off+0] = 0
|
||||
data[off+1] = 0
|
||||
data[off+2] = 0
|
||||
data[off+3] = 0
|
||||
}
|
||||
}
|
||||
|
||||
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create cursor pool: %w", err)
|
||||
}
|
||||
r.cursorPool = pool
|
||||
|
||||
wlBuf, err := pool.CreateBuffer(0, size, size, size*4, uint32(FormatARGB8888))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create cursor wl_buffer: %w", err)
|
||||
}
|
||||
r.cursorWlBuf = wlBuf
|
||||
|
||||
if err := surface.Attach(wlBuf, 0, 0); err != nil {
|
||||
return fmt.Errorf("attach cursor: %w", err)
|
||||
}
|
||||
if err := surface.Damage(0, 0, size, size); err != nil {
|
||||
return fmt.Errorf("damage cursor: %w", err)
|
||||
}
|
||||
if err := surface.Commit(); err != nil {
|
||||
return fmt.Errorf("commit cursor: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) createOutputSurface(output *WaylandOutput) (*OutputSurface, error) {
|
||||
surface, err := r.compositor.CreateSurface()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create surface: %w", err)
|
||||
}
|
||||
|
||||
layerSurf, err := r.layerShell.GetLayerSurface(
|
||||
surface,
|
||||
output.wlOutput,
|
||||
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
|
||||
"dms-screenshot",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get layer surface: %w", err)
|
||||
}
|
||||
|
||||
os := &OutputSurface{
|
||||
output: output,
|
||||
wlSurface: surface,
|
||||
layerSurf: layerSurf,
|
||||
}
|
||||
|
||||
if r.viewporter != nil {
|
||||
vp, err := r.viewporter.GetViewport(surface)
|
||||
if err == nil {
|
||||
os.viewport = vp
|
||||
}
|
||||
}
|
||||
|
||||
if err := layerSurf.SetAnchor(
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
|
||||
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("set anchor: %w", err)
|
||||
}
|
||||
if err := layerSurf.SetExclusiveZone(-1); err != nil {
|
||||
return nil, fmt.Errorf("set exclusive zone: %w", err)
|
||||
}
|
||||
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
|
||||
return nil, fmt.Errorf("set keyboard interactivity: %w", err)
|
||||
}
|
||||
|
||||
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
|
||||
if err := layerSurf.AckConfigure(e.Serial); err != nil {
|
||||
log.Error("ack configure failed", "err", err)
|
||||
return
|
||||
}
|
||||
os.logicalW = int(e.Width)
|
||||
os.logicalH = int(e.Height)
|
||||
os.configured = true
|
||||
r.captureForSurface(os)
|
||||
r.ensureShortcutsInhibitor(os)
|
||||
})
|
||||
|
||||
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
|
||||
r.running = false
|
||||
r.cancelled = true
|
||||
})
|
||||
|
||||
if err := surface.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("surface commit: %w", err)
|
||||
}
|
||||
|
||||
return os, nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) ensureShortcutsInhibitor(os *OutputSurface) {
|
||||
if r.shortcutsInhibitMgr == nil || r.seat == nil || r.shortcutsInhibitor != nil {
|
||||
return
|
||||
}
|
||||
inhibitor, err := r.shortcutsInhibitMgr.InhibitShortcuts(os.wlSurface, r.seat)
|
||||
if err == nil {
|
||||
r.shortcutsInhibitor = inhibitor
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) captureForSurface(os *OutputSurface) {
|
||||
pc := r.preCapture[os.output]
|
||||
if pc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
os.screenBuf = pc.screenBuf
|
||||
os.screenBufNoCursor = pc.screenBufNoCursor
|
||||
os.screenFormat = pc.format
|
||||
os.yInverted = pc.yInverted
|
||||
|
||||
r.initRenderBuffer(os)
|
||||
r.applyPreSelection(os)
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
|
||||
func (r *RegionSelector) initRenderBuffer(os *OutputSurface) {
|
||||
if os.screenBuf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
slot := &RenderSlot{}
|
||||
|
||||
buf, err := CreateShmBuffer(os.screenBuf.Width, os.screenBuf.Height, os.screenBuf.Stride)
|
||||
if err != nil {
|
||||
log.Error("create render slot buffer failed", "err", err)
|
||||
return
|
||||
}
|
||||
slot.shm = buf
|
||||
|
||||
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||
if err != nil {
|
||||
log.Error("create render slot pool failed", "err", err)
|
||||
buf.Close()
|
||||
return
|
||||
}
|
||||
slot.pool = pool
|
||||
|
||||
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), os.screenFormat)
|
||||
if err != nil {
|
||||
log.Error("create render slot wl_buffer failed", "err", err)
|
||||
pool.Destroy()
|
||||
buf.Close()
|
||||
return
|
||||
}
|
||||
slot.wlBuf = wlBuf
|
||||
|
||||
slotRef := slot
|
||||
wlBuf.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||
slotRef.busy = false
|
||||
})
|
||||
|
||||
os.slots[i] = slot
|
||||
}
|
||||
os.slotsReady = true
|
||||
}
|
||||
|
||||
func (os *OutputSurface) acquireFreeSlot() *RenderSlot {
|
||||
for _, slot := range os.slots {
|
||||
if slot != nil && !slot.busy {
|
||||
return slot
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RegionSelector) applyPreSelection(os *OutputSurface) {
|
||||
if r.preSelect.IsEmpty() || os.screenBuf == nil || r.selection.hasSelection {
|
||||
return
|
||||
}
|
||||
|
||||
if r.preSelect.Output != "" && r.preSelect.Output != os.output.name {
|
||||
return
|
||||
}
|
||||
|
||||
scaleX := float64(os.logicalW) / float64(os.screenBuf.Width)
|
||||
scaleY := float64(os.logicalH) / float64(os.screenBuf.Height)
|
||||
|
||||
x1 := float64(r.preSelect.X-os.output.x) * scaleX
|
||||
y1 := float64(r.preSelect.Y-os.output.y) * scaleY
|
||||
x2 := float64(r.preSelect.X-os.output.x+r.preSelect.Width) * scaleX
|
||||
y2 := float64(r.preSelect.Y-os.output.y+r.preSelect.Height) * scaleY
|
||||
|
||||
r.selection.hasSelection = true
|
||||
r.selection.dragging = false
|
||||
r.selection.surface = os
|
||||
r.selection.anchorX = x1
|
||||
r.selection.anchorY = y1
|
||||
r.selection.currentX = x2
|
||||
r.selection.currentY = y2
|
||||
r.activeSurface = os
|
||||
}
|
||||
|
||||
func (r *RegionSelector) getSourceBuffer(os *OutputSurface) *ShmBuffer {
|
||||
if !r.showCapturedCursor && os.screenBufNoCursor != nil {
|
||||
return os.screenBufNoCursor
|
||||
}
|
||||
return os.screenBuf
|
||||
}
|
||||
|
||||
func (r *RegionSelector) redrawSurface(os *OutputSurface) {
|
||||
srcBuf := r.getSourceBuffer(os)
|
||||
if srcBuf == nil || !os.slotsReady {
|
||||
return
|
||||
}
|
||||
|
||||
slot := os.acquireFreeSlot()
|
||||
if slot == nil {
|
||||
return
|
||||
}
|
||||
|
||||
slot.shm.CopyFrom(srcBuf)
|
||||
|
||||
// Draw overlay (dimming + selection) into this slot
|
||||
r.drawOverlay(os, slot.shm)
|
||||
|
||||
// Attach and commit (viewport only needs to be set once, but it's cheap)
|
||||
scale := os.output.scale
|
||||
if scale <= 0 {
|
||||
scale = 1
|
||||
}
|
||||
|
||||
if os.viewport != nil {
|
||||
srcW := float64(slot.shm.Width) / float64(scale)
|
||||
srcH := float64(slot.shm.Height) / float64(scale)
|
||||
_ = os.viewport.SetSource(0, 0, srcW, srcH)
|
||||
_ = os.viewport.SetDestination(int32(os.logicalW), int32(os.logicalH))
|
||||
}
|
||||
_ = os.wlSurface.SetBufferScale(scale)
|
||||
|
||||
_ = os.wlSurface.Attach(slot.wlBuf, 0, 0)
|
||||
_ = os.wlSurface.Damage(0, 0, int32(os.logicalW), int32(os.logicalH))
|
||||
_ = os.wlSurface.Commit()
|
||||
|
||||
// Mark this slot as busy until compositor releases it
|
||||
slot.busy = true
|
||||
}
|
||||
|
||||
func (r *RegionSelector) cleanup() {
|
||||
if r.cursorWlBuf != nil {
|
||||
r.cursorWlBuf.Destroy()
|
||||
}
|
||||
if r.cursorPool != nil {
|
||||
r.cursorPool.Destroy()
|
||||
}
|
||||
if r.cursorSurface != nil {
|
||||
r.cursorSurface.Destroy()
|
||||
}
|
||||
if r.cursorBuffer != nil {
|
||||
r.cursorBuffer.Close()
|
||||
}
|
||||
|
||||
for _, os := range r.surfaces {
|
||||
for _, slot := range os.slots {
|
||||
if slot == nil {
|
||||
continue
|
||||
}
|
||||
if slot.wlBuf != nil {
|
||||
slot.wlBuf.Destroy()
|
||||
}
|
||||
if slot.pool != nil {
|
||||
slot.pool.Destroy()
|
||||
}
|
||||
if slot.shm != nil {
|
||||
slot.shm.Close()
|
||||
}
|
||||
}
|
||||
if os.viewport != nil {
|
||||
os.viewport.Destroy()
|
||||
}
|
||||
if os.layerSurf != nil {
|
||||
os.layerSurf.Destroy()
|
||||
}
|
||||
if os.wlSurface != nil {
|
||||
os.wlSurface.Destroy()
|
||||
}
|
||||
if os.screenBuf != nil {
|
||||
os.screenBuf.Close()
|
||||
}
|
||||
if os.screenBufNoCursor != nil {
|
||||
os.screenBufNoCursor.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if r.shortcutsInhibitor != nil {
|
||||
_ = r.shortcutsInhibitor.Destroy()
|
||||
}
|
||||
if r.shortcutsInhibitMgr != nil {
|
||||
_ = r.shortcutsInhibitMgr.Destroy()
|
||||
}
|
||||
if r.viewporter != nil {
|
||||
r.viewporter.Destroy()
|
||||
}
|
||||
if r.screencopy != nil {
|
||||
r.screencopy.Destroy()
|
||||
}
|
||||
if r.pointer != nil {
|
||||
r.pointer.Release()
|
||||
}
|
||||
if r.keyboard != nil {
|
||||
r.keyboard.Release()
|
||||
}
|
||||
if r.display != nil {
|
||||
r.ctx.Close()
|
||||
}
|
||||
}
|
||||
264
core/internal/screenshot/region_input.go
Normal file
264
core/internal/screenshot/region_input.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
func (r *RegionSelector) setupInput() {
|
||||
if r.seat == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
||||
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && r.pointer == nil {
|
||||
if pointer, err := r.seat.GetPointer(); err == nil {
|
||||
r.pointer = pointer
|
||||
r.setupPointerHandlers()
|
||||
}
|
||||
}
|
||||
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && r.keyboard == nil {
|
||||
if keyboard, err := r.seat.GetKeyboard(); err == nil {
|
||||
r.keyboard = keyboard
|
||||
r.setupKeyboardHandlers()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RegionSelector) setupPointerHandlers() {
|
||||
r.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
|
||||
if r.cursorSurface != nil {
|
||||
_ = r.pointer.SetCursor(e.Serial, r.cursorSurface, 12, 12)
|
||||
}
|
||||
|
||||
r.activeSurface = nil
|
||||
for _, os := range r.surfaces {
|
||||
if os.wlSurface.ID() == e.Surface.ID() {
|
||||
r.activeSurface = os
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
r.pointerX = e.SurfaceX
|
||||
r.pointerY = e.SurfaceY
|
||||
})
|
||||
|
||||
r.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
|
||||
if r.activeSurface == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.pointerX = e.SurfaceX
|
||||
r.pointerY = e.SurfaceY
|
||||
|
||||
if !r.selection.dragging {
|
||||
return
|
||||
}
|
||||
|
||||
curX, curY := e.SurfaceX, e.SurfaceY
|
||||
if r.shiftHeld {
|
||||
dx := curX - r.selection.anchorX
|
||||
dy := curY - r.selection.anchorY
|
||||
adx, ady := dx, dy
|
||||
if adx < 0 {
|
||||
adx = -adx
|
||||
}
|
||||
if ady < 0 {
|
||||
ady = -ady
|
||||
}
|
||||
size := adx
|
||||
if ady > adx {
|
||||
size = ady
|
||||
}
|
||||
if dx < 0 {
|
||||
curX = r.selection.anchorX - size
|
||||
} else {
|
||||
curX = r.selection.anchorX + size
|
||||
}
|
||||
if dy < 0 {
|
||||
curY = r.selection.anchorY - size
|
||||
} else {
|
||||
curY = r.selection.anchorY + size
|
||||
}
|
||||
}
|
||||
|
||||
r.selection.currentX = curX
|
||||
r.selection.currentY = curY
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
})
|
||||
|
||||
r.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||
if r.activeSurface == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch e.Button {
|
||||
case 0x110: // BTN_LEFT
|
||||
switch e.State {
|
||||
case 1: // pressed
|
||||
r.preSelect = Region{}
|
||||
r.selection.hasSelection = true
|
||||
r.selection.dragging = true
|
||||
r.selection.surface = r.activeSurface
|
||||
r.selection.anchorX = r.pointerX
|
||||
r.selection.anchorY = r.pointerY
|
||||
r.selection.currentX = r.pointerX
|
||||
r.selection.currentY = r.pointerY
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
case 0: // released
|
||||
r.selection.dragging = false
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
}
|
||||
default:
|
||||
r.cancelled = true
|
||||
r.running = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RegionSelector) setupKeyboardHandlers() {
|
||||
r.keyboard.SetModifiersHandler(func(e client.KeyboardModifiersEvent) {
|
||||
r.shiftHeld = e.ModsDepressed&1 != 0
|
||||
})
|
||||
|
||||
r.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||
if e.State != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
switch e.Key {
|
||||
case 1:
|
||||
r.cancelled = true
|
||||
r.running = false
|
||||
case 25:
|
||||
r.showCapturedCursor = !r.showCapturedCursor
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
case 28, 57:
|
||||
if r.selection.hasSelection {
|
||||
r.finishSelection()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RegionSelector) finishSelection() {
|
||||
if r.selection.surface == nil {
|
||||
r.running = false
|
||||
return
|
||||
}
|
||||
|
||||
os := r.selection.surface
|
||||
srcBuf := r.getSourceBuffer(os)
|
||||
if srcBuf == nil {
|
||||
r.running = false
|
||||
return
|
||||
}
|
||||
|
||||
x1, y1 := r.selection.anchorX, r.selection.anchorY
|
||||
x2, y2 := r.selection.currentX, r.selection.currentY
|
||||
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
|
||||
scaleX, scaleY := 1.0, 1.0
|
||||
if os.logicalW > 0 {
|
||||
scaleX = float64(srcBuf.Width) / float64(os.logicalW)
|
||||
scaleY = float64(srcBuf.Height) / float64(os.logicalH)
|
||||
}
|
||||
|
||||
bx1 := int(x1 * scaleX)
|
||||
by1 := int(y1 * scaleY)
|
||||
bx2 := int(x2 * scaleX)
|
||||
by2 := int(y2 * scaleY)
|
||||
|
||||
// Clamp to buffer bounds
|
||||
if bx1 < 0 {
|
||||
bx1 = 0
|
||||
}
|
||||
if by1 < 0 {
|
||||
by1 = 0
|
||||
}
|
||||
if bx2 > srcBuf.Width {
|
||||
bx2 = srcBuf.Width
|
||||
}
|
||||
if by2 > srcBuf.Height {
|
||||
by2 = srcBuf.Height
|
||||
}
|
||||
|
||||
w, h := bx2-bx1+1, by2-by1+1
|
||||
if r.shiftHeld && w != h {
|
||||
if w < h {
|
||||
h = w
|
||||
} else {
|
||||
w = h
|
||||
}
|
||||
}
|
||||
if w < 1 {
|
||||
w = 1
|
||||
}
|
||||
if h < 1 {
|
||||
h = 1
|
||||
}
|
||||
|
||||
// Create cropped buffer and copy pixels directly
|
||||
cropped, err := CreateShmBuffer(w, h, w*4)
|
||||
if err != nil {
|
||||
r.running = false
|
||||
return
|
||||
}
|
||||
|
||||
srcData := srcBuf.Data()
|
||||
dstData := cropped.Data()
|
||||
for y := 0; y < h; y++ {
|
||||
srcY := by1 + y
|
||||
if srcY >= srcBuf.Height {
|
||||
break
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
srcX := bx1 + x
|
||||
if srcX >= srcBuf.Width {
|
||||
break
|
||||
}
|
||||
si := srcY*srcBuf.Stride + srcX*4
|
||||
di := y*cropped.Stride + x*4
|
||||
if si+3 < len(srcData) && di+3 < len(dstData) {
|
||||
dstData[di+0] = srcData[si+0]
|
||||
dstData[di+1] = srcData[si+1]
|
||||
dstData[di+2] = srcData[si+2]
|
||||
dstData[di+3] = srcData[si+3]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r.capturedBuffer = cropped
|
||||
r.capturedRegion = Region{
|
||||
X: int32(bx1),
|
||||
Y: int32(by1),
|
||||
Width: int32(w),
|
||||
Height: int32(h),
|
||||
Output: os.output.name,
|
||||
}
|
||||
|
||||
// Also store for "last region" feature with global coords
|
||||
r.result = Region{
|
||||
X: int32(bx1) + os.output.x,
|
||||
Y: int32(by1) + os.output.y,
|
||||
Width: int32(w),
|
||||
Height: int32(h),
|
||||
Output: os.output.name,
|
||||
}
|
||||
|
||||
r.running = false
|
||||
}
|
||||
322
core/internal/screenshot/region_render.go
Normal file
322
core/internal/screenshot/region_render.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package screenshot
|
||||
|
||||
import "fmt"
|
||||
|
||||
var fontGlyphs = map[rune][12]uint8{
|
||||
'0': {0x3C, 0x66, 0x66, 0x6E, 0x76, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||
'1': {0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00},
|
||||
'2': {0x3C, 0x66, 0x66, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x66, 0x7E, 0x00, 0x00},
|
||||
'3': {0x3C, 0x66, 0x06, 0x06, 0x1C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
|
||||
'4': {0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xCC, 0xFE, 0x0C, 0x0C, 0x1E, 0x00, 0x00},
|
||||
'5': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
|
||||
'6': {0x1C, 0x30, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||
'7': {0x7E, 0x66, 0x06, 0x06, 0x0C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00},
|
||||
'8': {0x3C, 0x66, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||
'9': {0x3C, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x0C, 0x38, 0x00, 0x00},
|
||||
'x': {0x00, 0x00, 0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, 0x00, 0x00},
|
||||
'E': {0x7E, 0x60, 0x60, 0x60, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x00, 0x00},
|
||||
'P': {0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00},
|
||||
'S': {0x3C, 0x66, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x06, 0x66, 0x3C, 0x00, 0x00},
|
||||
'a': {0x00, 0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
|
||||
'c': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x60, 0x60, 0x60, 0x66, 0x3C, 0x00, 0x00},
|
||||
'd': {0x00, 0x00, 0x06, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
|
||||
'e': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x7E, 0x60, 0x60, 0x3C, 0x00, 0x00},
|
||||
'h': {0x00, 0x60, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00},
|
||||
'i': {0x00, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00},
|
||||
'n': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00},
|
||||
'o': {0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00},
|
||||
'p': {0x00, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x00, 0x00},
|
||||
'r': {0x00, 0x00, 0x00, 0x6E, 0x76, 0x60, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00},
|
||||
's': {0x00, 0x00, 0x00, 0x3E, 0x60, 0x60, 0x3C, 0x06, 0x06, 0x7C, 0x00, 0x00},
|
||||
't': {0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00},
|
||||
'u': {0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3E, 0x00, 0x00},
|
||||
'w': {0x00, 0x00, 0x00, 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00, 0x00},
|
||||
'l': {0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00},
|
||||
' ': {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
':': {0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00},
|
||||
'/': {0x00, 0x02, 0x06, 0x0C, 0x18, 0x18, 0x30, 0x60, 0x40, 0x00, 0x00, 0x00},
|
||||
'[': {0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00},
|
||||
']': {0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00},
|
||||
}
|
||||
|
||||
type OverlayStyle struct {
|
||||
BackgroundR, BackgroundG, BackgroundB, BackgroundA uint8
|
||||
TextR, TextG, TextB uint8
|
||||
AccentR, AccentG, AccentB uint8
|
||||
}
|
||||
|
||||
var DefaultOverlayStyle = OverlayStyle{
|
||||
BackgroundR: 30, BackgroundG: 30, BackgroundB: 30, BackgroundA: 220,
|
||||
TextR: 255, TextG: 255, TextB: 255,
|
||||
AccentR: 100, AccentG: 180, AccentB: 255,
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawOverlay(os *OutputSurface, renderBuf *ShmBuffer) {
|
||||
data := renderBuf.Data()
|
||||
stride := renderBuf.Stride
|
||||
w, h := renderBuf.Width, renderBuf.Height
|
||||
format := os.screenFormat
|
||||
|
||||
// Dim the entire buffer
|
||||
for y := 0; y < h; y++ {
|
||||
off := y * stride
|
||||
for x := 0; x < w; x++ {
|
||||
i := off + x*4
|
||||
if i+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[i+0] = uint8(int(data[i+0]) * 3 / 5)
|
||||
data[i+1] = uint8(int(data[i+1]) * 3 / 5)
|
||||
data[i+2] = uint8(int(data[i+2]) * 3 / 5)
|
||||
}
|
||||
}
|
||||
|
||||
r.drawHUD(data, stride, w, h, format)
|
||||
|
||||
if !r.selection.hasSelection || r.selection.surface != os {
|
||||
return
|
||||
}
|
||||
|
||||
scaleX := float64(w) / float64(os.logicalW)
|
||||
scaleY := float64(h) / float64(os.logicalH)
|
||||
|
||||
bx1 := int(r.selection.anchorX * scaleX)
|
||||
by1 := int(r.selection.anchorY * scaleY)
|
||||
bx2 := int(r.selection.currentX * scaleX)
|
||||
by2 := int(r.selection.currentY * scaleY)
|
||||
|
||||
if bx1 > bx2 {
|
||||
bx1, bx2 = bx2, bx1
|
||||
}
|
||||
if by1 > by2 {
|
||||
by1, by2 = by2, by1
|
||||
}
|
||||
|
||||
bx1 = clamp(bx1, 0, w-1)
|
||||
by1 = clamp(by1, 0, h-1)
|
||||
bx2 = clamp(bx2, 0, w-1)
|
||||
by2 = clamp(by2, 0, h-1)
|
||||
|
||||
srcBuf := r.getSourceBuffer(os)
|
||||
srcData := srcBuf.Data()
|
||||
for y := by1; y <= by2; y++ {
|
||||
rowOff := y * stride
|
||||
for x := bx1; x <= bx2; x++ {
|
||||
si := y*srcBuf.Stride + x*4
|
||||
di := rowOff + x*4
|
||||
if si+3 >= len(srcData) || di+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[di+0] = srcData[si+0]
|
||||
data[di+1] = srcData[si+1]
|
||||
data[di+2] = srcData[si+2]
|
||||
data[di+3] = srcData[si+3]
|
||||
}
|
||||
}
|
||||
|
||||
selW, selH := bx2-bx1+1, by2-by1+1
|
||||
if r.shiftHeld && selW != selH {
|
||||
if selW < selH {
|
||||
selH = selW
|
||||
} else {
|
||||
selW = selH
|
||||
}
|
||||
}
|
||||
r.drawBorder(data, stride, w, h, bx1, by1, selW, selH, format)
|
||||
r.drawDimensions(data, stride, w, h, bx1, by1, selW, selH, format)
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uint32) {
|
||||
if r.selection.dragging {
|
||||
return
|
||||
}
|
||||
|
||||
style := LoadOverlayStyle()
|
||||
const charW, charH, padding, itemSpacing = 8, 12, 12, 24
|
||||
|
||||
cursorLabel := "hide"
|
||||
if !r.showCapturedCursor {
|
||||
cursorLabel = "show"
|
||||
}
|
||||
|
||||
items := []struct{ key, desc string }{
|
||||
{"Space/Enter", "capture"},
|
||||
{"P", cursorLabel + " cursor"},
|
||||
{"Esc", "cancel"},
|
||||
}
|
||||
|
||||
totalW := 0
|
||||
for i, item := range items {
|
||||
totalW += len(item.key)*(charW+1) + 4 + len(item.desc)*(charW+1)
|
||||
if i < len(items)-1 {
|
||||
totalW += itemSpacing
|
||||
}
|
||||
}
|
||||
|
||||
hudW := totalW + padding*2
|
||||
hudH := charH + padding*2
|
||||
hudX := (bufW - hudW) / 2
|
||||
hudY := bufH - hudH - 20
|
||||
|
||||
r.fillRect(data, stride, bufW, bufH, hudX, hudY, hudW, hudH,
|
||||
style.BackgroundR, style.BackgroundG, style.BackgroundB, style.BackgroundA, format)
|
||||
|
||||
tx, ty := hudX+padding, hudY+padding
|
||||
for i, item := range items {
|
||||
r.drawText(data, stride, bufW, bufH, tx, ty, item.key,
|
||||
style.AccentR, style.AccentG, style.AccentB, format)
|
||||
tx += len(item.key) * (charW + 1)
|
||||
|
||||
r.drawText(data, stride, bufW, bufH, tx, ty, " "+item.desc,
|
||||
style.TextR, style.TextG, style.TextB, format)
|
||||
tx += (1 + len(item.desc)) * (charW + 1)
|
||||
|
||||
if i < len(items)-1 {
|
||||
tx += itemSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawBorder(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) {
|
||||
const thickness = 2
|
||||
for i := 0; i < thickness; i++ {
|
||||
r.drawHLine(data, stride, bufW, bufH, x-i, y-i, w+2*i, format)
|
||||
r.drawHLine(data, stride, bufW, bufH, x-i, y+h+i-1, w+2*i, format)
|
||||
r.drawVLine(data, stride, bufW, bufH, x-i, y-i, h+2*i, format)
|
||||
r.drawVLine(data, stride, bufW, bufH, x+w+i-1, y-i, h+2*i, format)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawHLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) {
|
||||
if y < 0 || y >= bufH {
|
||||
return
|
||||
}
|
||||
rowOff := y * stride
|
||||
for i := 0; i < length; i++ {
|
||||
px := x + i
|
||||
if px < 0 || px >= bufW {
|
||||
continue
|
||||
}
|
||||
off := rowOff + px*4
|
||||
if off+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawVLine(data []byte, stride, bufW, bufH, x, y, length int, _ uint32) {
|
||||
if x < 0 || x >= bufW {
|
||||
return
|
||||
}
|
||||
for i := 0; i < length; i++ {
|
||||
py := y + i
|
||||
if py < 0 || py >= bufH {
|
||||
continue
|
||||
}
|
||||
off := py*stride + x*4
|
||||
if off+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[off], data[off+1], data[off+2], data[off+3] = 255, 255, 255, 255
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawDimensions(data []byte, stride, bufW, bufH, x, y, w, h int, format uint32) {
|
||||
text := fmt.Sprintf("%dx%d", w, h)
|
||||
|
||||
const charW, charH = 8, 12
|
||||
textW := len(text) * (charW + 1)
|
||||
textH := charH
|
||||
|
||||
tx := x + (w-textW)/2
|
||||
ty := y + h + 8
|
||||
|
||||
if ty+textH > bufH {
|
||||
ty = y - textH - 8
|
||||
}
|
||||
tx = clamp(tx, 0, bufW-textW)
|
||||
|
||||
r.fillRect(data, stride, bufW, bufH, tx-4, ty-2, textW+8, textH+4, 0, 0, 0, 200, format)
|
||||
r.drawText(data, stride, bufW, bufH, tx, ty, text, 255, 255, 255, format)
|
||||
}
|
||||
|
||||
func (r *RegionSelector) fillRect(data []byte, stride, bufW, bufH, x, y, w, h int, cr, cg, cb, ca uint8, format uint32) {
|
||||
alpha := float64(ca) / 255.0
|
||||
invAlpha := 1.0 - alpha
|
||||
|
||||
c0, c2 := cb, cr
|
||||
if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) {
|
||||
c0, c2 = cr, cb
|
||||
}
|
||||
|
||||
for py := y; py < y+h && py < bufH; py++ {
|
||||
if py < 0 {
|
||||
continue
|
||||
}
|
||||
for px := x; px < x+w && px < bufW; px++ {
|
||||
if px < 0 {
|
||||
continue
|
||||
}
|
||||
off := py*stride + px*4
|
||||
if off+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[off+0] = uint8(float64(data[off+0])*invAlpha + float64(c0)*alpha)
|
||||
data[off+1] = uint8(float64(data[off+1])*invAlpha + float64(cg)*alpha)
|
||||
data[off+2] = uint8(float64(data[off+2])*invAlpha + float64(c2)*alpha)
|
||||
data[off+3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawText(data []byte, stride, bufW, bufH, x, y int, text string, cr, cg, cb uint8, format uint32) {
|
||||
for i, ch := range text {
|
||||
r.drawChar(data, stride, bufW, bufH, x+i*9, y, ch, cr, cg, cb, format)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RegionSelector) drawChar(data []byte, stride, bufW, bufH, x, y int, ch rune, cr, cg, cb uint8, format uint32) {
|
||||
glyph, ok := fontGlyphs[ch]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c0, c2 := cb, cr
|
||||
if format == uint32(FormatABGR8888) || format == uint32(FormatXBGR8888) {
|
||||
c0, c2 = cr, cb
|
||||
}
|
||||
|
||||
for row := 0; row < 12; row++ {
|
||||
py := y + row
|
||||
if py < 0 || py >= bufH {
|
||||
continue
|
||||
}
|
||||
bits := glyph[row]
|
||||
for col := 0; col < 8; col++ {
|
||||
if (bits & (1 << (7 - col))) == 0 {
|
||||
continue
|
||||
}
|
||||
px := x + col
|
||||
if px < 0 || px >= bufW {
|
||||
continue
|
||||
}
|
||||
off := py*stride + px*4
|
||||
if off+3 >= len(data) {
|
||||
continue
|
||||
}
|
||||
data[off], data[off+1], data[off+2], data[off+3] = c0, cg, c2, 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clamp(v, lo, hi int) int {
|
||||
switch {
|
||||
case v < lo:
|
||||
return lo
|
||||
case v > hi:
|
||||
return hi
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
613
core/internal/screenshot/screenshot.go
Normal file
613
core/internal/screenshot/screenshot.go
Normal file
@@ -0,0 +1,613 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type WaylandOutput struct {
|
||||
wlOutput *client.Output
|
||||
globalName uint32
|
||||
name string
|
||||
x, y int32
|
||||
width int32
|
||||
height int32
|
||||
scale int32
|
||||
transform int32
|
||||
}
|
||||
|
||||
type CaptureResult struct {
|
||||
Buffer *ShmBuffer
|
||||
Region Region
|
||||
YInverted bool
|
||||
Format uint32
|
||||
}
|
||||
|
||||
type Screenshoter struct {
|
||||
config Config
|
||||
|
||||
display *client.Display
|
||||
registry *client.Registry
|
||||
ctx *client.Context
|
||||
|
||||
compositor *client.Compositor
|
||||
shm *client.Shm
|
||||
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||
|
||||
outputs map[uint32]*WaylandOutput
|
||||
outputsMu sync.Mutex
|
||||
}
|
||||
|
||||
func New(config Config) *Screenshoter {
|
||||
return &Screenshoter{
|
||||
config: config,
|
||||
outputs: make(map[uint32]*WaylandOutput),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screenshoter) Run() (*CaptureResult, error) {
|
||||
if err := s.connect(); err != nil {
|
||||
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer s.cleanup()
|
||||
|
||||
if err := s.setupRegistry(); err != nil {
|
||||
return nil, fmt.Errorf("registry setup: %w", err)
|
||||
}
|
||||
|
||||
if err := s.roundtrip(); err != nil {
|
||||
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||
}
|
||||
|
||||
if s.screencopy == nil {
|
||||
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||
}
|
||||
|
||||
if err := s.roundtrip(); err != nil {
|
||||
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||
}
|
||||
|
||||
switch s.config.Mode {
|
||||
case ModeLastRegion:
|
||||
return s.captureLastRegion()
|
||||
case ModeRegion:
|
||||
return s.captureRegion()
|
||||
case ModeWindow:
|
||||
return s.captureWindow()
|
||||
case ModeOutput:
|
||||
return s.captureOutput(s.config.OutputName)
|
||||
case ModeFullScreen:
|
||||
return s.captureFullScreen()
|
||||
case ModeAllScreens:
|
||||
return s.captureAllScreens()
|
||||
default:
|
||||
return s.captureRegion()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
||||
lastRegion := GetLastRegion()
|
||||
if lastRegion.IsEmpty() {
|
||||
return s.captureRegion()
|
||||
}
|
||||
|
||||
output := s.findOutputForRegion(lastRegion)
|
||||
if output == nil {
|
||||
return s.captureRegion()
|
||||
}
|
||||
|
||||
return s.captureRegionOnOutput(output, lastRegion)
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||
selector := NewRegionSelector(s)
|
||||
result, cancelled, err := selector.Run()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("region selection: %w", err)
|
||||
}
|
||||
if cancelled || result == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := SaveLastRegion(result.Region); err != nil {
|
||||
log.Debug("failed to save last region", "err", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
|
||||
geom, err := GetActiveWindow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
region := Region{
|
||||
X: geom.X,
|
||||
Y: geom.Y,
|
||||
Width: geom.Width,
|
||||
Height: geom.Height,
|
||||
}
|
||||
|
||||
output := s.findOutputForRegion(region)
|
||||
if output == nil {
|
||||
return nil, fmt.Errorf("could not find output for window")
|
||||
}
|
||||
|
||||
return s.captureRegionOnOutput(output, region)
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureFullScreen() (*CaptureResult, error) {
|
||||
output := s.findFocusedOutput()
|
||||
if output == nil {
|
||||
s.outputsMu.Lock()
|
||||
for _, o := range s.outputs {
|
||||
output = o
|
||||
break
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
}
|
||||
|
||||
if output == nil {
|
||||
return nil, fmt.Errorf("no output available")
|
||||
}
|
||||
|
||||
return s.captureWholeOutput(output)
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureOutput(name string) (*CaptureResult, error) {
|
||||
s.outputsMu.Lock()
|
||||
var output *WaylandOutput
|
||||
for _, o := range s.outputs {
|
||||
if o.name == name {
|
||||
output = o
|
||||
break
|
||||
}
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
|
||||
if output == nil {
|
||||
return nil, fmt.Errorf("output %q not found", name)
|
||||
}
|
||||
|
||||
return s.captureWholeOutput(output)
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
s.outputsMu.Lock()
|
||||
outputs := make([]*WaylandOutput, 0, len(s.outputs))
|
||||
var minX, minY, maxX, maxY int32
|
||||
first := true
|
||||
|
||||
for _, o := range s.outputs {
|
||||
outputs = append(outputs, o)
|
||||
right := o.x + o.width
|
||||
bottom := o.y + o.height
|
||||
|
||||
if first {
|
||||
minX, minY = o.x, o.y
|
||||
maxX, maxY = right, bottom
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
|
||||
if o.x < minX {
|
||||
minX = o.x
|
||||
}
|
||||
if o.y < minY {
|
||||
minY = o.y
|
||||
}
|
||||
if right > maxX {
|
||||
maxX = right
|
||||
}
|
||||
if bottom > maxY {
|
||||
maxY = bottom
|
||||
}
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
|
||||
if len(outputs) == 0 {
|
||||
return nil, fmt.Errorf("no outputs available")
|
||||
}
|
||||
|
||||
if len(outputs) == 1 {
|
||||
return s.captureWholeOutput(outputs[0])
|
||||
}
|
||||
|
||||
totalW := maxX - minX
|
||||
totalH := maxY - minY
|
||||
|
||||
compositeStride := int(totalW) * 4
|
||||
composite, err := CreateShmBuffer(int(totalW), int(totalH), compositeStride)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||
}
|
||||
|
||||
composite.Clear()
|
||||
|
||||
var format uint32
|
||||
for _, output := range outputs {
|
||||
result, err := s.captureWholeOutput(output)
|
||||
if err != nil {
|
||||
log.Warn("failed to capture output", "name", output.name, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if format == 0 {
|
||||
format = result.Format
|
||||
}
|
||||
s.blitBuffer(composite, result.Buffer, int(output.x-minX), int(output.y-minY), result.YInverted)
|
||||
result.Buffer.Close()
|
||||
}
|
||||
|
||||
return &CaptureResult{
|
||||
Buffer: composite,
|
||||
Region: Region{X: minX, Y: minY, Width: totalW, Height: totalH},
|
||||
Format: format,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
||||
srcData := src.Data()
|
||||
dstData := dst.Data()
|
||||
|
||||
for srcY := 0; srcY < src.Height; srcY++ {
|
||||
actualSrcY := srcY
|
||||
if yInverted {
|
||||
actualSrcY = src.Height - 1 - srcY
|
||||
}
|
||||
|
||||
dy := dstY + srcY
|
||||
if dy < 0 || dy >= dst.Height {
|
||||
continue
|
||||
}
|
||||
|
||||
srcRowOff := actualSrcY * src.Stride
|
||||
dstRowOff := dy * dst.Stride
|
||||
|
||||
for srcX := 0; srcX < src.Width; srcX++ {
|
||||
dx := dstX + srcX
|
||||
if dx < 0 || dx >= dst.Width {
|
||||
continue
|
||||
}
|
||||
|
||||
si := srcRowOff + srcX*4
|
||||
di := dstRowOff + dx*4
|
||||
|
||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||
continue
|
||||
}
|
||||
|
||||
dstData[di+0] = srcData[si+0]
|
||||
dstData[di+1] = srcData[si+1]
|
||||
dstData[di+2] = srcData[si+2]
|
||||
dstData[di+3] = srcData[si+3]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
|
||||
cursor := int32(0)
|
||||
if s.config.IncludeCursor {
|
||||
cursor = 1
|
||||
}
|
||||
|
||||
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("capture output: %w", err)
|
||||
}
|
||||
|
||||
return s.processFrame(frame, Region{
|
||||
X: output.x,
|
||||
Y: output.y,
|
||||
Width: output.width,
|
||||
Height: output.height,
|
||||
Output: output.name,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||
localX := region.X - output.x
|
||||
localY := region.Y - output.y
|
||||
|
||||
cursor := int32(0)
|
||||
if s.config.IncludeCursor {
|
||||
cursor = 1
|
||||
}
|
||||
|
||||
frame, err := s.screencopy.CaptureOutputRegion(
|
||||
cursor,
|
||||
output.wlOutput,
|
||||
localX, localY,
|
||||
region.Width, region.Height,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("capture region: %w", err)
|
||||
}
|
||||
|
||||
return s.processFrame(frame, region)
|
||||
}
|
||||
|
||||
func (s *Screenshoter) processFrame(frame *wlr_screencopy.ZwlrScreencopyFrameV1, region Region) (*CaptureResult, error) {
|
||||
var buf *ShmBuffer
|
||||
var format PixelFormat
|
||||
var yInverted bool
|
||||
ready := false
|
||||
failed := false
|
||||
|
||||
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||
var err error
|
||||
buf, err = CreateShmBuffer(int(e.Width), int(e.Height), int(e.Stride))
|
||||
if err != nil {
|
||||
log.Error("failed to create buffer", "err", err)
|
||||
return
|
||||
}
|
||||
format = PixelFormat(e.Format)
|
||||
buf.Format = format
|
||||
})
|
||||
|
||||
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
|
||||
yInverted = (e.Flags & 1) != 0
|
||||
})
|
||||
|
||||
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
|
||||
if buf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pool, err := s.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||
if err != nil {
|
||||
log.Error("failed to create pool", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), uint32(format))
|
||||
if err != nil {
|
||||
pool.Destroy()
|
||||
log.Error("failed to create wl_buffer", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := frame.Copy(wlBuf); err != nil {
|
||||
log.Error("failed to copy frame", "err", err)
|
||||
}
|
||||
|
||||
pool.Destroy()
|
||||
})
|
||||
|
||||
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||
ready = true
|
||||
})
|
||||
|
||||
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
|
||||
failed = true
|
||||
})
|
||||
|
||||
for !ready && !failed {
|
||||
if err := s.ctx.Dispatch(); err != nil {
|
||||
frame.Destroy()
|
||||
return nil, fmt.Errorf("dispatch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
frame.Destroy()
|
||||
|
||||
if failed {
|
||||
if buf != nil {
|
||||
buf.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("frame capture failed")
|
||||
}
|
||||
|
||||
return &CaptureResult{
|
||||
Buffer: buf,
|
||||
Region: region,
|
||||
YInverted: yInverted,
|
||||
Format: uint32(format),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) findOutputForRegion(region Region) *WaylandOutput {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
|
||||
cx := region.X + region.Width/2
|
||||
cy := region.Y + region.Height/2
|
||||
|
||||
for _, o := range s.outputs {
|
||||
if cx >= o.x && cx < o.x+o.width && cy >= o.y && cy < o.y+o.height {
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
for _, o := range s.outputs {
|
||||
if region.X >= o.x && region.X < o.x+o.width &&
|
||||
region.Y >= o.y && region.Y < o.y+o.height {
|
||||
return o
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) findFocusedOutput() *WaylandOutput {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
for _, o := range s.outputs {
|
||||
return o
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) connect() error {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.display = display
|
||||
s.ctx = display.Context()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) roundtrip() error {
|
||||
return wlhelpers.Roundtrip(s.display, s.ctx)
|
||||
}
|
||||
|
||||
func (s *Screenshoter) setupRegistry() error {
|
||||
registry, err := s.display.GetRegistry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.registry = registry
|
||||
|
||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||
s.handleGlobal(e)
|
||||
})
|
||||
|
||||
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||
s.outputsMu.Lock()
|
||||
delete(s.outputs, e.Name)
|
||||
s.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) handleGlobal(e client.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case client.CompositorInterfaceName:
|
||||
comp := client.NewCompositor(s.ctx)
|
||||
if err := s.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
|
||||
s.compositor = comp
|
||||
}
|
||||
|
||||
case client.ShmInterfaceName:
|
||||
shm := client.NewShm(s.ctx)
|
||||
if err := s.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||
s.shm = shm
|
||||
}
|
||||
|
||||
case client.OutputInterfaceName:
|
||||
output := client.NewOutput(s.ctx)
|
||||
version := e.Version
|
||||
if version > 4 {
|
||||
version = 4
|
||||
}
|
||||
if err := s.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||
s.outputsMu.Lock()
|
||||
s.outputs[e.Name] = &WaylandOutput{
|
||||
wlOutput: output,
|
||||
globalName: e.Name,
|
||||
scale: 1,
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
s.setupOutputHandlers(e.Name, output)
|
||||
}
|
||||
|
||||
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||
sc := wlr_screencopy.NewZwlrScreencopyManagerV1(s.ctx)
|
||||
version := e.Version
|
||||
if version > 3 {
|
||||
version = 3
|
||||
}
|
||||
if err := s.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
|
||||
s.screencopy = sc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screenshoter) setupOutputHandlers(name uint32, output *client.Output) {
|
||||
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||
s.outputsMu.Lock()
|
||||
if o, ok := s.outputs[name]; ok {
|
||||
o.x, o.y = e.X, e.Y
|
||||
o.transform = int32(e.Transform)
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||
return
|
||||
}
|
||||
s.outputsMu.Lock()
|
||||
if o, ok := s.outputs[name]; ok {
|
||||
o.width, o.height = e.Width, e.Height
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||
s.outputsMu.Lock()
|
||||
if o, ok := s.outputs[name]; ok {
|
||||
o.scale = e.Factor
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
})
|
||||
|
||||
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||
s.outputsMu.Lock()
|
||||
if o, ok := s.outputs[name]; ok {
|
||||
o.name = e.Name
|
||||
}
|
||||
s.outputsMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Screenshoter) cleanup() {
|
||||
if s.screencopy != nil {
|
||||
s.screencopy.Destroy()
|
||||
}
|
||||
if s.display != nil {
|
||||
s.ctx.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screenshoter) GetOutputs() []*WaylandOutput {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
out := make([]*WaylandOutput, 0, len(s.outputs))
|
||||
for _, o := range s.outputs {
|
||||
out = append(out, o)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ListOutputs() ([]Output, error) {
|
||||
sc := New(DefaultConfig())
|
||||
if err := sc.connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sc.cleanup()
|
||||
|
||||
if err := sc.setupRegistry(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sc.roundtrip(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := sc.roundtrip(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sc.outputsMu.Lock()
|
||||
defer sc.outputsMu.Unlock()
|
||||
|
||||
result := make([]Output, 0, len(sc.outputs))
|
||||
for _, o := range sc.outputs {
|
||||
result = append(result, Output{
|
||||
Name: o.name,
|
||||
X: o.x,
|
||||
Y: o.y,
|
||||
Width: o.width,
|
||||
Height: o.height,
|
||||
Scale: o.scale,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
18
core/internal/screenshot/shm.go
Normal file
18
core/internal/screenshot/shm.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package screenshot
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||
|
||||
type PixelFormat = shm.PixelFormat
|
||||
|
||||
const (
|
||||
FormatARGB8888 = shm.FormatARGB8888
|
||||
FormatXRGB8888 = shm.FormatXRGB8888
|
||||
FormatABGR8888 = shm.FormatABGR8888
|
||||
FormatXBGR8888 = shm.FormatXBGR8888
|
||||
)
|
||||
|
||||
type ShmBuffer = shm.Buffer
|
||||
|
||||
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||
return shm.CreateBuffer(width, height, stride)
|
||||
}
|
||||
65
core/internal/screenshot/state.go
Normal file
65
core/internal/screenshot/state.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type PersistentState struct {
|
||||
LastRegion Region `json:"last_region"`
|
||||
}
|
||||
|
||||
func getStateFilePath() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
cacheDir = path.Join(os.Getenv("HOME"), ".cache")
|
||||
}
|
||||
return filepath.Join(cacheDir, "dms", "screenshot-state.json")
|
||||
}
|
||||
|
||||
func LoadState() (*PersistentState, error) {
|
||||
path := getStateFilePath()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &PersistentState{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var state PersistentState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return &PersistentState{}, nil
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func SaveState(state *PersistentState) error {
|
||||
path := getStateFilePath()
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
func GetLastRegion() Region {
|
||||
state, err := LoadState()
|
||||
if err != nil {
|
||||
return Region{}
|
||||
}
|
||||
return state.LastRegion
|
||||
}
|
||||
|
||||
func SaveLastRegion(r Region) error {
|
||||
state, _ := LoadState()
|
||||
state.LastRegion = r
|
||||
return SaveState(state)
|
||||
}
|
||||
127
core/internal/screenshot/theme.go
Normal file
127
core/internal/screenshot/theme.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ThemeColors struct {
|
||||
Background string `json:"surface"`
|
||||
OnSurface string `json:"on_surface"`
|
||||
Primary string `json:"primary"`
|
||||
}
|
||||
|
||||
type ColorScheme struct {
|
||||
Dark ThemeColors `json:"dark"`
|
||||
Light ThemeColors `json:"light"`
|
||||
}
|
||||
|
||||
type ColorsFile struct {
|
||||
Colors ColorScheme `json:"colors"`
|
||||
}
|
||||
|
||||
var cachedStyle *OverlayStyle
|
||||
|
||||
func LoadOverlayStyle() OverlayStyle {
|
||||
if cachedStyle != nil {
|
||||
return *cachedStyle
|
||||
}
|
||||
|
||||
style := DefaultOverlayStyle
|
||||
colors := loadColorsFile()
|
||||
if colors == nil {
|
||||
cachedStyle = &style
|
||||
return style
|
||||
}
|
||||
|
||||
theme := &colors.Dark
|
||||
if isLightMode() {
|
||||
theme = &colors.Light
|
||||
}
|
||||
|
||||
if bg, ok := parseHexColor(theme.Background); ok {
|
||||
style.BackgroundR, style.BackgroundG, style.BackgroundB = bg[0], bg[1], bg[2]
|
||||
}
|
||||
if text, ok := parseHexColor(theme.OnSurface); ok {
|
||||
style.TextR, style.TextG, style.TextB = text[0], text[1], text[2]
|
||||
}
|
||||
if accent, ok := parseHexColor(theme.Primary); ok {
|
||||
style.AccentR, style.AccentG, style.AccentB = accent[0], accent[1], accent[2]
|
||||
}
|
||||
|
||||
cachedStyle = &style
|
||||
return style
|
||||
}
|
||||
|
||||
func loadColorsFile() *ColorScheme {
|
||||
path := getColorsFilePath()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var file ColorsFile
|
||||
if err := json.Unmarshal(data, &file); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &file.Colors
|
||||
}
|
||||
|
||||
func getColorsFilePath() string {
|
||||
cacheDir := os.Getenv("XDG_CACHE_HOME")
|
||||
if cacheDir == "" {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
return ""
|
||||
}
|
||||
cacheDir = filepath.Join(home, ".cache")
|
||||
}
|
||||
return filepath.Join(cacheDir, "DankMaterialShell", "dms-colors.json")
|
||||
}
|
||||
|
||||
func isLightMode() bool {
|
||||
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
scheme := strings.TrimSpace(string(out))
|
||||
switch scheme {
|
||||
case "'prefer-light'", "'default'":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseHexColor(hex string) ([3]uint8, bool) {
|
||||
hex = strings.TrimPrefix(hex, "#")
|
||||
if len(hex) != 6 {
|
||||
return [3]uint8{}, false
|
||||
}
|
||||
|
||||
var r, g, b uint8
|
||||
for i, ptr := range []*uint8{&r, &g, &b} {
|
||||
val := 0
|
||||
for j := 0; j < 2; j++ {
|
||||
c := hex[i*2+j]
|
||||
val *= 16
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
val += int(c - '0')
|
||||
case c >= 'a' && c <= 'f':
|
||||
val += int(c - 'a' + 10)
|
||||
case c >= 'A' && c <= 'F':
|
||||
val += int(c - 'A' + 10)
|
||||
default:
|
||||
return [3]uint8{}, false
|
||||
}
|
||||
}
|
||||
*ptr = uint8(val)
|
||||
}
|
||||
|
||||
return [3]uint8{r, g, b}, true
|
||||
}
|
||||
68
core/internal/screenshot/types.go
Normal file
68
core/internal/screenshot/types.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package screenshot
|
||||
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
ModeRegion Mode = iota
|
||||
ModeWindow
|
||||
ModeFullScreen
|
||||
ModeAllScreens
|
||||
ModeOutput
|
||||
ModeLastRegion
|
||||
)
|
||||
|
||||
type Format int
|
||||
|
||||
const (
|
||||
FormatPNG Format = iota
|
||||
FormatJPEG
|
||||
FormatPPM
|
||||
)
|
||||
|
||||
type Region struct {
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
Width int32 `json:"width"`
|
||||
Height int32 `json:"height"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
func (r Region) IsEmpty() bool {
|
||||
return r.Width <= 0 || r.Height <= 0
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Name string
|
||||
X, Y int32
|
||||
Width int32
|
||||
Height int32
|
||||
Scale int32
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mode Mode
|
||||
OutputName string
|
||||
IncludeCursor bool
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
Filename string
|
||||
Clipboard bool
|
||||
SaveFile bool
|
||||
Notify bool
|
||||
Stdout bool
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Mode: ModeRegion,
|
||||
IncludeCursor: false,
|
||||
Format: FormatPNG,
|
||||
Quality: 90,
|
||||
OutputDir: "",
|
||||
Filename: "",
|
||||
Clipboard: true,
|
||||
SaveFile: true,
|
||||
Notify: true,
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -261,8 +265,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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
26
core/internal/wayland/client/helpers.go
Normal file
26
core/internal/wayland/client/helpers.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
core/internal/wayland/shm/buffer.go
Normal file
139
core/internal/wayland/shm/buffer.go
Normal 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)
|
||||
}
|
||||
13
core/pkg/go-wayland/wayland/stable/xdg-shell/xdg_shell.go
Normal file
13
core/pkg/go-wayland/wayland/stable/xdg-shell/xdg_shell.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -66,7 +66,7 @@ in {
|
||||
};
|
||||
|
||||
Service = {
|
||||
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
|
||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -619,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
|
||||
@@ -639,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]}"
|
||||
@@ -664,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
64
flake.nix
64
flake.nix
@@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,396 +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
|
||||
property var persistentModal: null
|
||||
|
||||
readonly property bool hasActiveModal: activeModal !== null
|
||||
readonly property bool hasPersistentModal: persistentModal !== null
|
||||
readonly property bool isPersistentModalActive: hasActiveModal && activeModal === persistentModal
|
||||
readonly property bool shouldShowModal: hasActiveModal
|
||||
readonly property bool shouldKeepWindowsAlive: hasPersistentModal
|
||||
|
||||
onPersistentModalChanged: {
|
||||
if (!persistentModal)
|
||||
return;
|
||||
cachedModal = persistentModal;
|
||||
cachedModalWidth = Theme.px(persistentModal.modalWidth, dpr);
|
||||
cachedModalHeight = Theme.px(persistentModal.modalHeight, dpr);
|
||||
cachedModalX = calculateX(persistentModal);
|
||||
cachedModalY = calculateY(persistentModal);
|
||||
cachedAnimationDuration = persistentModal.animationDuration ?? Theme.shortDuration;
|
||||
cachedEnterCurve = persistentModal.animationEnterCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
|
||||
cachedExitCurve = persistentModal.animationExitCurve ?? Theme.expressiveCurves.expressiveFastSpatial;
|
||||
cachedScaleCollapsed = persistentModal.animationScaleCollapsed ?? 0.96;
|
||||
if (persistentModal.directContent) {
|
||||
persistentModal.directContent.parent = directContentWrapper;
|
||||
persistentModal.directContent.anchors.fill = directContentWrapper;
|
||||
}
|
||||
}
|
||||
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 || root.shouldKeepWindowsAlive
|
||||
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 || root.shouldKeepWindowsAlive
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
quickshell/Common/KeyUtils.js
Normal file
144
quickshell/Common/KeyUtils.js
Normal 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;
|
||||
}
|
||||
495
quickshell/Common/KeybindActions.js
Normal file
495
quickshell/Common/KeybindActions.js
Normal 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" }
|
||||
];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -248,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
|
||||
@@ -295,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
|
||||
@@ -311,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
|
||||
@@ -367,7 +372,8 @@ Singleton {
|
||||
openOnOverview: false,
|
||||
visible: true,
|
||||
popupGapsAuto: true,
|
||||
popupGapsManual: 4
|
||||
popupGapsManual: 4,
|
||||
maximizeDetection: true
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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 },
|
||||
@@ -147,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 },
|
||||
@@ -194,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 },
|
||||
@@ -210,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 },
|
||||
@@ -265,7 +270,8 @@ var SPEC = {
|
||||
openOnOverview: false,
|
||||
visible: true,
|
||||
popupGapsAuto: true,
|
||||
popupGapsManual: 4
|
||||
popupGapsManual: 4,
|
||||
maximizeDetection: true
|
||||
}], onChange: "updateBarConfigs" }
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ DankModal {
|
||||
modalWidth: 520
|
||||
modalHeight: 500
|
||||
|
||||
onBackgroundClicked: close()
|
||||
|
||||
onDialogClosed: {
|
||||
searchQuery = ""
|
||||
selectedIndex = 0
|
||||
keyboardNavigationActive: false
|
||||
keyboardNavigationActive = false
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -12,6 +13,11 @@ DankModal {
|
||||
layerNamespace: "dms:power-menu"
|
||||
keepPopoutsOpen: true
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [root.contentWindow]
|
||||
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
|
||||
}
|
||||
|
||||
property int selectedIndex: 0
|
||||
property int selectedRow: 0
|
||||
property int selectedCol: 0
|
||||
@@ -269,11 +275,34 @@ DankModal {
|
||||
} else {
|
||||
selectedIndex = defaultIndex;
|
||||
}
|
||||
Qt.callLater(() => modalFocusScope.forceActiveFocus());
|
||||
}
|
||||
onDialogClosed: () => {
|
||||
cancelHold();
|
||||
}
|
||||
Component.onCompleted: updateVisibleActions()
|
||||
modalFocusScope.Keys.onPressed: event => {
|
||||
if (event.isAutoRepeat) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (SettingsData.powerMenuGridLayout) {
|
||||
handleGridNavigation(event, true);
|
||||
} else {
|
||||
handleListNavigation(event, true);
|
||||
}
|
||||
}
|
||||
modalFocusScope.Keys.onReleased: event => {
|
||||
if (event.isAutoRepeat) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (SettingsData.powerMenuGridLayout) {
|
||||
handleGridNavigation(event, false);
|
||||
} else {
|
||||
handleListNavigation(event, false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleListNavigation(event, isPressed) {
|
||||
if (!isPressed) {
|
||||
@@ -452,33 +481,10 @@ DankModal {
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
implicitHeight: (SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + Theme.spacingL * 2 + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0)
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.isAutoRepeat) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (SettingsData.powerMenuGridLayout)
|
||||
root.handleGridNavigation(event, true);
|
||||
else
|
||||
root.handleListNavigation(event, true);
|
||||
}
|
||||
|
||||
Keys.onReleased: event => {
|
||||
if (event.isAutoRepeat) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (SettingsData.powerMenuGridLayout)
|
||||
root.handleGridNavigation(event, false);
|
||||
else
|
||||
root.handleListNavigation(event, false);
|
||||
}
|
||||
|
||||
Grid {
|
||||
id: buttonGrid
|
||||
visible: SettingsData.powerMenuGridLayout
|
||||
@@ -781,12 +787,18 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
readonly property int remainingSeconds: Math.ceil(SettingsData.powerActionHoldDuration * (1 - root.holdProgress))
|
||||
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
|
||||
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
||||
text: {
|
||||
if (root.showHoldHint)
|
||||
return I18n.tr("Hold longer to confirm");
|
||||
if (root.holdProgress > 0)
|
||||
return I18n.tr("Hold to confirm (%1s)").arg(remainingSeconds);
|
||||
if (root.holdProgress > 0) {
|
||||
if (totalMs < 1000)
|
||||
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
|
||||
return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
|
||||
}
|
||||
if (totalMs < 1000)
|
||||
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
|
||||
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
|
||||
@@ -1,866 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: powerTab
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingL
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: lockScreenSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "lock"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Lock Screen")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Power Actions")
|
||||
description: I18n.tr("Show power, restart, and logout buttons on the lock screen")
|
||||
checked: SettingsData.lockScreenShowPowerActions
|
||||
onToggled: checked => SettingsData.set("lockScreenShowPowerActions", checked)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.warning
|
||||
visible: !SessionService.loginctlAvailable
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Enable loginctl lock integration")
|
||||
description: I18n.tr("Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen")
|
||||
checked: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
enabled: SessionService.loginctlAvailable
|
||||
onToggled: checked => {
|
||||
if (SessionService.loginctlAvailable) {
|
||||
SettingsData.set("loginctlLockIntegration", checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Lock before suspend")
|
||||
description: I18n.tr("Automatically lock the screen when the system prepares to suspend")
|
||||
checked: SettingsData.lockBeforeSuspend
|
||||
visible: SessionService.loginctlAvailable && SettingsData.loginctlLockIntegration
|
||||
onToggled: checked => SettingsData.set("lockBeforeSuspend", checked)
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Enable fingerprint authentication")
|
||||
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
|
||||
checked: SettingsData.enableFprint
|
||||
visible: SettingsData.fprintdAvailable
|
||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: timeoutSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: timeoutSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "schedule"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Idle Settings")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, parent.width - parent.children[0].width - parent.children[1].width - powerCategory.width - Theme.spacingM * 3)
|
||||
height: parent.height
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: powerCategory
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: BatteryService.batteryAvailable
|
||||
model: ["AC Power", "Battery"]
|
||||
currentIndex: 0
|
||||
selectionMode: "single"
|
||||
checkEnabled: false
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Prevent idle for media")
|
||||
description: I18n.tr("Inhibit idle timeout when audio or video is playing")
|
||||
checked: SettingsData.preventIdleForMedia
|
||||
visible: IdleService.idleMonitorAvailable
|
||||
onToggled: checked => SettingsData.set("preventIdleForMedia", checked)
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Fade to lock screen")
|
||||
description: I18n.tr("Gradually fade the screen before locking with a configurable grace period")
|
||||
checked: SettingsData.fadeToLockEnabled
|
||||
onToggled: checked => SettingsData.set("fadeToLockEnabled", checked)
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: fadeGracePeriodDropdown
|
||||
property var periodOptions: ["1 second", "2 seconds", "3 seconds", "4 seconds", "5 seconds", "10 seconds", "15 seconds", "20 seconds", "30 seconds"]
|
||||
property var periodValues: [1, 2, 3, 4, 5, 10, 15, 20, 30]
|
||||
|
||||
width: parent.width
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Fade grace period")
|
||||
options: periodOptions
|
||||
visible: SettingsData.fadeToLockEnabled
|
||||
enabled: SettingsData.fadeToLockEnabled
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentPeriod = SettingsData.fadeToLockGracePeriod;
|
||||
const index = periodValues.indexOf(currentPeriod);
|
||||
currentValue = index >= 0 ? periodOptions[index] : "5 seconds";
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = periodOptions.indexOf(value);
|
||||
if (index >= 0) {
|
||||
SettingsData.set("fadeToLockGracePeriod", periodValues[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: lockDropdown
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Automatically lock after")
|
||||
options: timeoutOptions
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout;
|
||||
const index = lockDropdown.timeoutValues.indexOf(currentTimeout);
|
||||
lockDropdown.currentValue = index >= 0 ? lockDropdown.timeoutOptions[index] : "Never";
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acLockTimeout : SettingsData.batteryLockTimeout;
|
||||
const index = timeoutValues.indexOf(currentTimeout);
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never";
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value);
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index];
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acLockTimeout", timeout);
|
||||
} else {
|
||||
SettingsData.set("batteryLockTimeout", timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: monitorDropdown
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Turn off monitors after")
|
||||
options: timeoutOptions
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout;
|
||||
const index = monitorDropdown.timeoutValues.indexOf(currentTimeout);
|
||||
monitorDropdown.currentValue = index >= 0 ? monitorDropdown.timeoutOptions[index] : "Never";
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acMonitorTimeout : SettingsData.batteryMonitorTimeout;
|
||||
const index = timeoutValues.indexOf(currentTimeout);
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never";
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value);
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index];
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acMonitorTimeout", timeout);
|
||||
} else {
|
||||
SettingsData.set("batteryMonitorTimeout", timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: suspendDropdown
|
||||
property var timeoutOptions: ["Never", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes", "30 minutes", "1 hour", "1 hour 30 minutes", "2 hours", "3 hours"]
|
||||
property var timeoutValues: [0, 60, 120, 180, 300, 600, 900, 1200, 1800, 3600, 5400, 7200, 10800]
|
||||
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Suspend system after")
|
||||
options: timeoutOptions
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout;
|
||||
const index = suspendDropdown.timeoutValues.indexOf(currentTimeout);
|
||||
suspendDropdown.currentValue = index >= 0 ? suspendDropdown.timeoutOptions[index] : "Never";
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentTimeout = powerCategory.currentIndex === 0 ? SettingsData.acSuspendTimeout : SettingsData.batterySuspendTimeout;
|
||||
const index = timeoutValues.indexOf(currentTimeout);
|
||||
currentValue = index >= 0 ? timeoutOptions[index] : "Never";
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = timeoutOptions.indexOf(value);
|
||||
if (index >= 0) {
|
||||
const timeout = timeoutValues[index];
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendTimeout", timeout);
|
||||
} else {
|
||||
SettingsData.set("batterySuspendTimeout", timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: SessionService.hibernateSupported
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Suspend behavior")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
leftPadding: Theme.spacingM
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: suspendBehaviorSelector
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
model: ["Suspend", "Hibernate", "Suspend then Hibernate"]
|
||||
selectionMode: "single"
|
||||
checkEnabled: false
|
||||
|
||||
Connections {
|
||||
target: powerCategory
|
||||
function onCurrentIndexChanged() {
|
||||
const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior;
|
||||
suspendBehaviorSelector.currentIndex = behavior;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const behavior = powerCategory.currentIndex === 0 ? SettingsData.acSuspendBehavior : SettingsData.batterySuspendBehavior;
|
||||
currentIndex = behavior;
|
||||
}
|
||||
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected) {
|
||||
if (powerCategory.currentIndex === 0) {
|
||||
SettingsData.set("acSuspendBehavior", index);
|
||||
} else {
|
||||
SettingsData.set("batterySuspendBehavior", index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Idle monitoring not supported - requires newer Quickshell version")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !IdleService.idleMonitorAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: powerMenuCustomSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "tune"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Menu Customization")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Customize which actions appear in the power menu")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Use Grid Layout")
|
||||
description: I18n.tr("Display power menu actions in a grid instead of a list")
|
||||
checked: SettingsData.powerMenuGridLayout
|
||||
onToggled: checked => SettingsData.set("powerMenuGridLayout", checked)
|
||||
}
|
||||
|
||||
DankDropdown {
|
||||
id: defaultActionDropdown
|
||||
width: parent.width
|
||||
addHorizontalPadding: true
|
||||
text: I18n.tr("Default selected action")
|
||||
options: ["Reboot", "Log Out", "Power Off", "Lock", "Suspend", "Restart DMS", "Hibernate"]
|
||||
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"]
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentAction = SettingsData.powerMenuDefaultAction || "logout";
|
||||
const index = actionValues.indexOf(currentAction);
|
||||
currentValue = index >= 0 ? options[index] : "Log Out";
|
||||
}
|
||||
|
||||
onValueChanged: value => {
|
||||
const index = options.indexOf(value);
|
||||
if (index >= 0) {
|
||||
SettingsData.set("powerMenuDefaultAction", actionValues[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Reboot")
|
||||
checked: SettingsData.powerMenuActions.includes("reboot")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
if (checked && !actions.includes("reboot")) {
|
||||
actions.push("reboot");
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "reboot");
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Log Out")
|
||||
checked: SettingsData.powerMenuActions.includes("logout")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
if (checked && !actions.includes("logout")) {
|
||||
actions.push("logout");
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "logout");
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Power Off")
|
||||
checked: SettingsData.powerMenuActions.includes("poweroff")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
if (checked && !actions.includes("poweroff")) {
|
||||
actions.push("poweroff");
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "poweroff");
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Lock")
|
||||
checked: SettingsData.powerMenuActions.includes("lock")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
if (checked && !actions.includes("lock")) {
|
||||
actions.push("lock");
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "lock");
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Suspend")
|
||||
checked: SettingsData.powerMenuActions.includes("suspend")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
if (checked && !actions.includes("suspend")) {
|
||||
actions.push("suspend");
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "suspend");
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Restart DMS")
|
||||
description: I18n.tr("Restart the DankMaterialShell")
|
||||
checked: SettingsData.powerMenuActions.includes("restart")
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
if (checked && !actions.includes("restart")) {
|
||||
actions.push("restart");
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "restart");
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Show Hibernate")
|
||||
description: I18n.tr("Only visible if hibernate is supported by your system")
|
||||
checked: SettingsData.powerMenuActions.includes("hibernate")
|
||||
visible: SessionService.hibernateSupported
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
if (checked && !actions.includes("hibernate")) {
|
||||
actions.push("hibernate");
|
||||
} else if (!checked) {
|
||||
actions = actions.filter(a => a !== "hibernate");
|
||||
}
|
||||
SettingsData.set("powerMenuActions", actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: powerCommandConfirmSection
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Power Action Confirmation")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Hold to Confirm Power Actions")
|
||||
description: I18n.tr("Require holding button/key to confirm power off, restart, suspend, hibernate and logout")
|
||||
checked: SettingsData.powerActionConfirm
|
||||
onToggled: checked => SettingsData.set("powerActionConfirm", checked)
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: SettingsData.powerActionConfirm
|
||||
|
||||
Item {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: holdDurationLabel.height
|
||||
|
||||
StyledText {
|
||||
id: holdDurationLabel
|
||||
text: I18n.tr("Hold Duration")
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.weight: Font.Medium
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: SettingsData.powerActionHoldDuration + "s"
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.weight: Font.Medium
|
||||
color: Theme.primary
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
minimum: 1
|
||||
maximum: 10
|
||||
unit: "s"
|
||||
wheelEnabled: false
|
||||
showValue: false
|
||||
thumbOutlineColor: Theme.surfaceContainerHigh
|
||||
value: SettingsData.powerActionHoldDuration
|
||||
onSliderValueChanged: newValue => SettingsData.set("powerActionHoldDuration", newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: powerCommandCustomization
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "developer_mode"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Custom Power Actions")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard lock procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customLockCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myLock.sh"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionLock) {
|
||||
text = SettingsData.customPowerActionLock;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionLock", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard logout procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customLogoutCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myLogout.sh"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionLogout) {
|
||||
text = SettingsData.customPowerActionLogout;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionLogout", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard suspend procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customSuspendCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/mySuspend.sh"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionSuspend) {
|
||||
text = SettingsData.customPowerActionSuspend;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionSuspend", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard hibernate procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customHibernateCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myHibernate.sh"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionHibernate) {
|
||||
text = SettingsData.customPowerActionHibernate;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionHibernate", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard reboot procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customRebootCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myReboot.sh"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionReboot) {
|
||||
text = SettingsData.customPowerActionReboot;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionReboot", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Command or script to run instead of the standard power off procedure")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: customPowerOffCommand
|
||||
width: parent.width
|
||||
height: 48
|
||||
placeholderText: "/usr/bin/myPowerOff.sh"
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.customPowerActionPowerOff) {
|
||||
text = SettingsData.customPowerActionPowerOff;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: {
|
||||
SettingsData.set("customPowerActionPowerOff", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,14 @@ FocusScope {
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: personalizationLoader
|
||||
|
||||
id: wallpaperLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 0
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: Component {
|
||||
PersonalizationTab {
|
||||
WallpaperTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
}
|
||||
@@ -41,7 +40,6 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: timeWeatherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 1
|
||||
visible: active
|
||||
@@ -57,13 +55,30 @@ FocusScope {
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: topBarLoader
|
||||
|
||||
id: keybindsLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 2
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: KeybindsTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: topBarLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 3
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: DankBarTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
@@ -76,14 +91,13 @@ FocusScope {
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: widgetsLoader
|
||||
|
||||
id: workspacesLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 3
|
||||
active: root.currentIndex === 4
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: WidgetTweaksTab {}
|
||||
sourceComponent: WorkspacesTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
@@ -94,9 +108,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 4
|
||||
active: root.currentIndex === 5
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -113,9 +126,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: displaysLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 5
|
||||
active: root.currentIndex === 6
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -130,9 +142,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: networkLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 6
|
||||
active: root.currentIndex === 7
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -147,9 +158,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: printerLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 7
|
||||
active: root.currentIndex === 8
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -164,9 +174,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: launcherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 8
|
||||
active: root.currentIndex === 9
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -181,9 +190,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: themeColorsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 9
|
||||
active: root.currentIndex === 10
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -197,14 +205,13 @@ FocusScope {
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: powerLoader
|
||||
|
||||
id: lockScreenLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 10
|
||||
active: root.currentIndex === 11
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: PowerSettings {}
|
||||
sourceComponent: LockScreenTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
@@ -215,9 +222,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: pluginsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 11
|
||||
active: root.currentIndex === 12
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -234,9 +240,8 @@ FocusScope {
|
||||
|
||||
Loader {
|
||||
id: aboutLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 12
|
||||
active: root.currentIndex === 13
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
@@ -248,5 +253,151 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: typographyMotionLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 14
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: TypographyMotionTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: soundsLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 15
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: SoundsTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: mediaPlayerLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 16
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: MediaPlayerTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: notificationsLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 17
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: NotificationsTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: osdLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 18
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: OSDTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: runningAppsLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 19
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: RunningAppsTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: systemUpdaterLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 20
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: SystemUpdaterTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: powerSleepLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 21
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: PowerSleepTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: widgetsLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 22
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: WidgetsTab {
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ FloatingWindow {
|
||||
|
||||
property alias profileBrowser: profileBrowser
|
||||
property alias wallpaperBrowser: wallpaperBrowser
|
||||
property alias sidebar: sidebar
|
||||
property int currentTabIndex: 0
|
||||
property bool shouldHaveFocus: visible
|
||||
property bool allowFocusOverride: false
|
||||
@@ -32,6 +33,23 @@ FloatingWindow {
|
||||
visible = !visible;
|
||||
}
|
||||
|
||||
function showWithTab(tabIndex: int) {
|
||||
if (tabIndex >= 0)
|
||||
currentTabIndex = tabIndex;
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function showWithTabName(tabName: string) {
|
||||
var idx = sidebar.resolveTabIndex(tabName);
|
||||
if (idx >= 0)
|
||||
currentTabIndex = idx;
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function resolveTabIndex(tabName: string): int {
|
||||
return sidebar.resolveTabIndex(tabName);
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
enableAnimations = true;
|
||||
menuVisible = !menuVisible;
|
||||
@@ -41,7 +59,7 @@ FloatingWindow {
|
||||
title: I18n.tr("Settings", "settings window title")
|
||||
minimumSize: Qt.size(500, 400)
|
||||
implicitWidth: 800
|
||||
implicitHeight: 800
|
||||
implicitHeight: 940
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
visible: false
|
||||
|
||||
@@ -121,29 +139,18 @@ FloatingWindow {
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
const tabCount = 13;
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
hide();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_Down) {
|
||||
currentTabIndex = (currentTabIndex + 1) % tabCount;
|
||||
if (event.key === Qt.Key_Down || (event.key === Qt.Key_Tab && !event.modifiers)) {
|
||||
sidebar.navigateNext();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_Up) {
|
||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_Tab && !event.modifiers) {
|
||||
currentTabIndex = (currentTabIndex + 1) % tabCount;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
|
||||
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount;
|
||||
if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
|
||||
sidebar.navigatePrevious();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,97 +7,383 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: sidebarContainer
|
||||
id: root
|
||||
|
||||
property int currentIndex: 0
|
||||
property var parentModal: null
|
||||
readonly property var allSidebarItems: [
|
||||
property var expandedCategories: ({})
|
||||
property var autoExpandedCategories: ({})
|
||||
|
||||
readonly property var categoryStructure: [
|
||||
{
|
||||
"id": "personalization",
|
||||
"text": I18n.tr("Personalization"),
|
||||
"icon": "person",
|
||||
"tabIndex": 0
|
||||
},
|
||||
{
|
||||
"text": I18n.tr("Time & Weather"),
|
||||
"icon": "schedule",
|
||||
"tabIndex": 1
|
||||
"icon": "palette",
|
||||
"children": [
|
||||
{
|
||||
"id": "wallpaper",
|
||||
"text": I18n.tr("Wallpaper"),
|
||||
"icon": "wallpaper",
|
||||
"tabIndex": 0
|
||||
},
|
||||
{
|
||||
"id": "theme",
|
||||
"text": I18n.tr("Theme & Colors"),
|
||||
"icon": "format_paint",
|
||||
"tabIndex": 10
|
||||
},
|
||||
{
|
||||
"id": "typography",
|
||||
"text": I18n.tr("Typography & Motion"),
|
||||
"icon": "text_fields",
|
||||
"tabIndex": 14
|
||||
},
|
||||
{
|
||||
"id": "time_weather",
|
||||
"text": I18n.tr("Time & Weather"),
|
||||
"icon": "schedule",
|
||||
"tabIndex": 1
|
||||
},
|
||||
{
|
||||
"id": "sounds",
|
||||
"text": I18n.tr("Sounds"),
|
||||
"icon": "volume_up",
|
||||
"tabIndex": 15,
|
||||
"soundsOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dankbar",
|
||||
"text": I18n.tr("Dank Bar"),
|
||||
"icon": "toolbar",
|
||||
"tabIndex": 2
|
||||
"children": [
|
||||
{
|
||||
"id": "dankbar_settings",
|
||||
"text": I18n.tr("Settings"),
|
||||
"icon": "tune",
|
||||
"tabIndex": 3
|
||||
},
|
||||
{
|
||||
"id": "dankbar_widgets",
|
||||
"text": I18n.tr("Widgets"),
|
||||
"icon": "widgets",
|
||||
"tabIndex": 22
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": I18n.tr("Widgets"),
|
||||
"icon": "widgets",
|
||||
"tabIndex": 3
|
||||
"id": "workspaces_widgets",
|
||||
"text": I18n.tr("Workspaces & Widgets"),
|
||||
"icon": "dashboard",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "workspaces",
|
||||
"text": I18n.tr("Workspaces"),
|
||||
"icon": "view_module",
|
||||
"tabIndex": 4
|
||||
},
|
||||
{
|
||||
"id": "media_player",
|
||||
"text": I18n.tr("Media Player"),
|
||||
"icon": "music_note",
|
||||
"tabIndex": 16
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"text": I18n.tr("Notifications"),
|
||||
"icon": "notifications",
|
||||
"tabIndex": 17
|
||||
},
|
||||
{
|
||||
"id": "osd",
|
||||
"text": I18n.tr("On-screen Displays"),
|
||||
"icon": "tune",
|
||||
"tabIndex": 18
|
||||
},
|
||||
{
|
||||
"id": "running_apps",
|
||||
"text": I18n.tr("Running Apps"),
|
||||
"icon": "apps",
|
||||
"tabIndex": 19,
|
||||
"hyprlandNiriOnly": true
|
||||
},
|
||||
{
|
||||
"id": "updater",
|
||||
"text": I18n.tr("System Updater"),
|
||||
"icon": "refresh",
|
||||
"tabIndex": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": I18n.tr("Dock"),
|
||||
"icon": "dock_to_bottom",
|
||||
"tabIndex": 4
|
||||
"id": "dock_launcher",
|
||||
"text": I18n.tr("Dock & Launcher"),
|
||||
"icon": "apps",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "dock",
|
||||
"text": I18n.tr("Dock"),
|
||||
"icon": "dock_to_bottom",
|
||||
"tabIndex": 5
|
||||
},
|
||||
{
|
||||
"id": "launcher",
|
||||
"text": I18n.tr("Launcher"),
|
||||
"icon": "grid_view",
|
||||
"tabIndex": 9
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "keybinds",
|
||||
"text": I18n.tr("Keyboard Shortcuts"),
|
||||
"icon": "keyboard",
|
||||
"tabIndex": 2,
|
||||
"shortcutsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "displays",
|
||||
"text": I18n.tr("Displays"),
|
||||
"icon": "monitor",
|
||||
"tabIndex": 5
|
||||
},
|
||||
{
|
||||
"text": I18n.tr("Network"),
|
||||
"icon": "wifi",
|
||||
"dmsOnly": true,
|
||||
"tabIndex": 6
|
||||
},
|
||||
{
|
||||
"id": "network",
|
||||
"text": I18n.tr("Network"),
|
||||
"icon": "wifi",
|
||||
"tabIndex": 7,
|
||||
"dmsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "printers",
|
||||
"text": I18n.tr("Printers"),
|
||||
"icon": "print",
|
||||
"cupsOnly": true,
|
||||
"tabIndex": 7
|
||||
},
|
||||
{
|
||||
"text": I18n.tr("Launcher"),
|
||||
"icon": "apps",
|
||||
"tabIndex": 8
|
||||
},
|
||||
{
|
||||
"text": I18n.tr("Theme & Colors"),
|
||||
"icon": "palette",
|
||||
"tabIndex": 9
|
||||
"tabIndex": 8,
|
||||
"cupsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "power_security",
|
||||
"text": I18n.tr("Power & Security"),
|
||||
"icon": "power",
|
||||
"tabIndex": 10
|
||||
"icon": "security",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "lock_screen",
|
||||
"text": I18n.tr("Lock Screen"),
|
||||
"icon": "lock",
|
||||
"tabIndex": 11
|
||||
},
|
||||
{
|
||||
"id": "power_sleep",
|
||||
"text": I18n.tr("Power & Sleep"),
|
||||
"icon": "power_settings_new",
|
||||
"tabIndex": 21
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "plugins",
|
||||
"text": I18n.tr("Plugins"),
|
||||
"icon": "extension",
|
||||
"tabIndex": 11
|
||||
"tabIndex": 12
|
||||
},
|
||||
{
|
||||
"id": "separator",
|
||||
"separator": true
|
||||
},
|
||||
{
|
||||
"id": "about",
|
||||
"text": I18n.tr("About"),
|
||||
"icon": "info",
|
||||
"tabIndex": 12
|
||||
"tabIndex": 13
|
||||
}
|
||||
]
|
||||
readonly property var sidebarItems: allSidebarItems.filter(item => {
|
||||
|
||||
function isItemVisible(item) {
|
||||
if (item.dmsOnly && NetworkService.usingLegacy)
|
||||
return false;
|
||||
if (item.cupsOnly && !CupsService.cupsAvailable)
|
||||
return false;
|
||||
if (item.shortcutsOnly && !KeybindsService.available)
|
||||
return false;
|
||||
if (item.soundsOnly && !AudioService.soundsAvailable)
|
||||
return false;
|
||||
if (item.hyprlandNiriOnly && !CompositorService.isNiri && !CompositorService.isHyprland)
|
||||
return false;
|
||||
return true;
|
||||
})
|
||||
}
|
||||
|
||||
function hasVisibleChildren(category) {
|
||||
if (!category.children)
|
||||
return false;
|
||||
return category.children.some(child => isItemVisible(child));
|
||||
}
|
||||
|
||||
function isCategoryVisible(category) {
|
||||
if (category.separator)
|
||||
return true;
|
||||
if (!isItemVisible(category))
|
||||
return false;
|
||||
if (category.children && !hasVisibleChildren(category))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleCategory(categoryId) {
|
||||
var newExpanded = Object.assign({}, expandedCategories);
|
||||
newExpanded[categoryId] = !isCategoryExpanded(categoryId);
|
||||
expandedCategories = newExpanded;
|
||||
|
||||
var newAutoExpanded = Object.assign({}, autoExpandedCategories);
|
||||
delete newAutoExpanded[categoryId];
|
||||
autoExpandedCategories = newAutoExpanded;
|
||||
}
|
||||
|
||||
function isCategoryExpanded(categoryId) {
|
||||
if (expandedCategories[categoryId] !== undefined) {
|
||||
return expandedCategories[categoryId];
|
||||
}
|
||||
var category = categoryStructure.find(cat => cat.id === categoryId);
|
||||
if (category && category.collapsedByDefault) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isChildActive(category) {
|
||||
if (!category.children)
|
||||
return false;
|
||||
return category.children.some(child => child.tabIndex === currentIndex);
|
||||
}
|
||||
|
||||
function findParentCategory(tabIndex) {
|
||||
for (var i = 0; i < categoryStructure.length; i++) {
|
||||
var cat = categoryStructure[i];
|
||||
if (cat.children) {
|
||||
for (var j = 0; j < cat.children.length; j++) {
|
||||
if (cat.children[j].tabIndex === tabIndex) {
|
||||
return cat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function autoExpandForTab(tabIndex) {
|
||||
var parent = findParentCategory(tabIndex);
|
||||
if (!parent)
|
||||
return;
|
||||
|
||||
if (!isCategoryExpanded(parent.id)) {
|
||||
var newExpanded = Object.assign({}, expandedCategories);
|
||||
newExpanded[parent.id] = true;
|
||||
expandedCategories = newExpanded;
|
||||
|
||||
var newAutoExpanded = Object.assign({}, autoExpandedCategories);
|
||||
newAutoExpanded[parent.id] = true;
|
||||
autoExpandedCategories = newAutoExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
function autoCollapseIfNeeded(oldTabIndex, newTabIndex) {
|
||||
var oldParent = findParentCategory(oldTabIndex);
|
||||
var newParent = findParentCategory(newTabIndex);
|
||||
|
||||
if (oldParent && oldParent !== newParent && autoExpandedCategories[oldParent.id]) {
|
||||
var newExpanded = Object.assign({}, expandedCategories);
|
||||
newExpanded[oldParent.id] = false;
|
||||
expandedCategories = newExpanded;
|
||||
|
||||
var newAutoExpanded = Object.assign({}, autoExpandedCategories);
|
||||
delete newAutoExpanded[oldParent.id];
|
||||
autoExpandedCategories = newAutoExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateNext() {
|
||||
const currentItemIndex = sidebarItems.findIndex(item => item.tabIndex === currentIndex);
|
||||
const nextIndex = (currentItemIndex + 1) % sidebarItems.length;
|
||||
currentIndex = sidebarItems[nextIndex].tabIndex;
|
||||
var flatItems = getFlatNavigableItems();
|
||||
var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
|
||||
var oldIndex = currentIndex;
|
||||
if (currentPos === -1) {
|
||||
currentIndex = flatItems[0]?.tabIndex ?? 0;
|
||||
} else {
|
||||
var nextPos = (currentPos + 1) % flatItems.length;
|
||||
currentIndex = flatItems[nextPos].tabIndex;
|
||||
}
|
||||
autoCollapseIfNeeded(oldIndex, currentIndex);
|
||||
autoExpandForTab(currentIndex);
|
||||
}
|
||||
|
||||
function navigatePrevious() {
|
||||
const currentItemIndex = sidebarItems.findIndex(item => item.tabIndex === currentIndex);
|
||||
const prevIndex = (currentItemIndex - 1 + sidebarItems.length) % sidebarItems.length;
|
||||
currentIndex = sidebarItems[prevIndex].tabIndex;
|
||||
var flatItems = getFlatNavigableItems();
|
||||
var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
|
||||
var oldIndex = currentIndex;
|
||||
if (currentPos === -1) {
|
||||
currentIndex = flatItems[0]?.tabIndex ?? 0;
|
||||
} else {
|
||||
var prevPos = (currentPos - 1 + flatItems.length) % flatItems.length;
|
||||
currentIndex = flatItems[prevPos].tabIndex;
|
||||
}
|
||||
autoCollapseIfNeeded(oldIndex, currentIndex);
|
||||
autoExpandForTab(currentIndex);
|
||||
}
|
||||
|
||||
function getFlatNavigableItems() {
|
||||
var items = [];
|
||||
for (var i = 0; i < categoryStructure.length; i++) {
|
||||
var cat = categoryStructure[i];
|
||||
if (cat.separator || !isCategoryVisible(cat))
|
||||
continue;
|
||||
|
||||
if (cat.tabIndex !== undefined && !cat.children) {
|
||||
items.push(cat);
|
||||
}
|
||||
|
||||
if (cat.children) {
|
||||
for (var j = 0; j < cat.children.length; j++) {
|
||||
var child = cat.children[j];
|
||||
if (isItemVisible(child)) {
|
||||
items.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function resolveTabIndex(name: string): int {
|
||||
if (!name)
|
||||
return -1;
|
||||
|
||||
var normalized = name.toLowerCase().replace(/[_\-\s]/g, "");
|
||||
|
||||
for (var i = 0; i < categoryStructure.length; i++) {
|
||||
var cat = categoryStructure[i];
|
||||
if (cat.separator)
|
||||
continue;
|
||||
|
||||
var catId = (cat.id || "").toLowerCase().replace(/[_\-\s]/g, "");
|
||||
if (catId === normalized) {
|
||||
if (cat.tabIndex !== undefined)
|
||||
return cat.tabIndex;
|
||||
if (cat.children && cat.children.length > 0)
|
||||
return cat.children[0].tabIndex;
|
||||
}
|
||||
|
||||
if (cat.children) {
|
||||
for (var j = 0; j < cat.children.length; j++) {
|
||||
var child = cat.children[j];
|
||||
var childId = (child.id || "").toLowerCase().replace(/[_\-\s]/g, "");
|
||||
if (childId === normalized)
|
||||
return child.tabIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
width: 270
|
||||
@@ -112,17 +398,16 @@ Rectangle {
|
||||
|
||||
Column {
|
||||
id: sidebarColumn
|
||||
|
||||
width: parent.width
|
||||
leftPadding: Theme.spacingS
|
||||
rightPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingL
|
||||
topPadding: Theme.spacingM + 2
|
||||
spacing: Theme.spacingXS
|
||||
spacing: 2
|
||||
|
||||
ProfileSection {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
parentModal: sidebarContainer.parentModal
|
||||
parentModal: root.parentModal
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -134,62 +419,194 @@ Rectangle {
|
||||
|
||||
Item {
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: Theme.spacingL
|
||||
height: Theme.spacingM
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: sidebarRepeater
|
||||
model: root.categoryStructure
|
||||
|
||||
model: sidebarContainer.sidebarItems
|
||||
|
||||
delegate: Rectangle {
|
||||
delegate: Column {
|
||||
id: categoryDelegate
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
property bool isActive: sidebarContainer.currentIndex === modelData.tabIndex
|
||||
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||
visible: root.isCategoryVisible(modelData)
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
visible: categoryDelegate.modelData.separator === true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || ""
|
||||
size: Theme.iconSize - 2
|
||||
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Theme.spacingS
|
||||
visible: categoryDelegate.modelData.separator === true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: categoryRow
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
visible: categoryDelegate.modelData.separator !== true
|
||||
color: {
|
||||
var hasTab = categoryDelegate.modelData.tabIndex !== undefined && !categoryDelegate.modelData.children;
|
||||
var isActive = hasTab && root.currentIndex === categoryDelegate.modelData.tabIndex;
|
||||
if (isActive)
|
||||
return Theme.primary;
|
||||
if (categoryMouseArea.containsMouse)
|
||||
return Theme.surfaceHover;
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: parent.parent.isActive ? Theme.primaryText : Theme.surfaceText
|
||||
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: categoryDelegate.modelData.icon || ""
|
||||
size: Theme.iconSize - 2
|
||||
color: {
|
||||
var hasTab = categoryDelegate.modelData.tabIndex !== undefined && !categoryDelegate.modelData.children;
|
||||
var isActive = hasTab && root.currentIndex === categoryDelegate.modelData.tabIndex;
|
||||
return isActive ? Theme.primaryText : Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: categoryDelegate.modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: {
|
||||
var hasTab = categoryDelegate.modelData.tabIndex !== undefined && !categoryDelegate.modelData.children;
|
||||
var isActive = hasTab && root.currentIndex === categoryDelegate.modelData.tabIndex;
|
||||
var childActive = root.isChildActive(categoryDelegate.modelData);
|
||||
return (isActive || childActive) ? Font.Medium : Font.Normal;
|
||||
}
|
||||
color: {
|
||||
var hasTab = categoryDelegate.modelData.tabIndex !== undefined && !categoryDelegate.modelData.children;
|
||||
var isActive = hasTab && root.currentIndex === categoryDelegate.modelData.tabIndex;
|
||||
return isActive ? Theme.primaryText : Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSize - Theme.spacingM - (categoryDelegate.modelData.children ? expandIcon.width + Theme.spacingS : 0)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: expandIcon
|
||||
name: root.isCategoryExpanded(categoryDelegate.modelData.id) ? "expand_less" : "expand_more"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: categoryDelegate.modelData.children !== undefined && categoryDelegate.modelData.children.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: categoryMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (categoryDelegate.modelData.children) {
|
||||
root.toggleCategory(categoryDelegate.modelData.id);
|
||||
} else if (categoryDelegate.modelData.tabIndex !== undefined) {
|
||||
root.currentIndex = categoryDelegate.modelData.tabIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
Column {
|
||||
id: childrenColumn
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
visible: categoryDelegate.modelData.children !== undefined && root.isCategoryExpanded(categoryDelegate.modelData.id)
|
||||
clip: true
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: () => {
|
||||
sidebarContainer.currentIndex = modelData.tabIndex;
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: categoryDelegate.modelData.children || []
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
delegate: Rectangle {
|
||||
id: childDelegate
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
width: childrenColumn.width
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
visible: root.isItemVisible(modelData)
|
||||
color: {
|
||||
var isActive = root.currentIndex === modelData.tabIndex;
|
||||
if (isActive)
|
||||
return Theme.primary;
|
||||
if (childMouseArea.containsMouse)
|
||||
return Theme.surfaceHover;
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL + Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: childDelegate.modelData.icon || ""
|
||||
size: Theme.iconSize - 4
|
||||
color: {
|
||||
var isActive = root.currentIndex === childDelegate.modelData.tabIndex;
|
||||
return isActive ? Theme.primaryText : Theme.surfaceVariantText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: childDelegate.modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeSmall + 1
|
||||
font.weight: root.currentIndex === childDelegate.modelData.tabIndex ? Font.Medium : Font.Normal
|
||||
color: {
|
||||
var isActive = root.currentIndex === childDelegate.modelData.tabIndex;
|
||||
return isActive ? Theme.primaryText : Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: childMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentIndex = childDelegate.modelData.tabIndex;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,31 @@ Rectangle {
|
||||
property var fileSearchController: null
|
||||
|
||||
function resetScroll() {
|
||||
filesList.contentY = 0
|
||||
filesList.contentY = 0;
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 32
|
||||
z: 100
|
||||
visible: filesList.contentHeight > filesList.height && (filesList.currentIndex < filesList.count - 1 || filesList.contentY < filesList.contentHeight - filesList.height - 1)
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: filesList
|
||||
|
||||
@@ -30,18 +49,22 @@ Rectangle {
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count)
|
||||
return
|
||||
|
||||
const itemY = index * (itemHeight + itemSpacing)
|
||||
const itemBottom = itemY + itemHeight
|
||||
return;
|
||||
const itemY = index * (itemHeight + itemSpacing);
|
||||
const itemBottom = itemY + itemHeight;
|
||||
const fadeHeight = 32;
|
||||
const isLastItem = index === count - 1;
|
||||
if (itemY < contentY)
|
||||
contentY = itemY
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height
|
||||
contentY = itemY;
|
||||
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
|
||||
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: 1
|
||||
model: fileSearchController ? fileSearchController.model : null
|
||||
currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1
|
||||
clip: true
|
||||
@@ -53,26 +76,26 @@ Rectangle {
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive)
|
||||
ensureVisible(currentIndex)
|
||||
ensureVisible(currentIndex);
|
||||
}
|
||||
|
||||
onItemClicked: function (index) {
|
||||
if (fileSearchController) {
|
||||
const item = fileSearchController.model.get(index)
|
||||
fileSearchController.openFile(item.filePath)
|
||||
const item = fileSearchController.model.get(index);
|
||||
fileSearchController.openFile(item.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
onItemRightClicked: function (index) {
|
||||
if (fileSearchController) {
|
||||
const item = fileSearchController.model.get(index)
|
||||
fileSearchController.openFolder(item.filePath)
|
||||
const item = fileSearchController.model.get(index);
|
||||
fileSearchController.openFolder(item.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyboardNavigationReset: {
|
||||
if (fileSearchController)
|
||||
fileSearchController.keyboardNavigationActive = false
|
||||
fileSearchController.keyboardNavigationActive = false;
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
@@ -86,7 +109,7 @@ Rectangle {
|
||||
width: ListView.view.width
|
||||
height: filesList.itemHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: ListView.isCurrentItem ? Theme.primaryPressed : fileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
color: ListView.isCurrentItem ? Theme.widgetBaseHoverColor : fileMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
@@ -109,16 +132,16 @@ Rectangle {
|
||||
id: nerdIcon
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
const lowerName = fileName.toLowerCase()
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile"))
|
||||
return "docker"
|
||||
return "docker";
|
||||
if (lowerName.startsWith("makefile"))
|
||||
return "makefile"
|
||||
return "makefile";
|
||||
if (lowerName.startsWith("license"))
|
||||
return "license"
|
||||
return "license";
|
||||
if (lowerName.startsWith("readme"))
|
||||
return "readme"
|
||||
return fileExtension.toLowerCase()
|
||||
return "readme";
|
||||
return fileExtension.toLowerCase();
|
||||
}
|
||||
size: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
@@ -196,18 +219,18 @@ Rectangle {
|
||||
z: 10
|
||||
onEntered: {
|
||||
if (filesList.hoverUpdatesSelection && !filesList.keyboardNavigationActive)
|
||||
filesList.currentIndex = index
|
||||
filesList.currentIndex = index;
|
||||
}
|
||||
onPositionChanged: {
|
||||
filesList.keyboardNavigationReset()
|
||||
filesList.keyboardNavigationReset();
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
filesList.itemClicked(index)
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
filesList.itemRightClicked(index)
|
||||
}
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
filesList.itemClicked(index);
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
filesList.itemRightClicked(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,21 +242,21 @@ Rectangle {
|
||||
StyledText {
|
||||
property string displayText: {
|
||||
if (!fileSearchController) {
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
if (!DSearchService.dsearchAvailable) {
|
||||
return I18n.tr("DankSearch not available")
|
||||
return I18n.tr("DankSearch not available");
|
||||
}
|
||||
if (fileSearchController.isSearching) {
|
||||
return I18n.tr("Searching...")
|
||||
return I18n.tr("Searching...");
|
||||
}
|
||||
if (fileSearchController.searchQuery.length === 0) {
|
||||
return I18n.tr("Enter a search query")
|
||||
return I18n.tr("Enter a search query");
|
||||
}
|
||||
if (!fileSearchController.model || fileSearchController.model.count === 0) {
|
||||
return I18n.tr("No files found")
|
||||
return I18n.tr("No files found");
|
||||
}
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
|
||||
text: displayText
|
||||
|
||||
@@ -51,21 +51,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (!activeFocus)
|
||||
return;
|
||||
if (!searchField)
|
||||
return;
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
|
||||
if (menu?.visible) {
|
||||
menu.handleKey(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide();
|
||||
@@ -211,6 +197,7 @@ Item {
|
||||
|
||||
parent: spotlightKeyHandler
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
parentHandler: spotlightKeyHandler
|
||||
searchField: spotlightKeyHandler.searchField
|
||||
visible: false
|
||||
z: 1000
|
||||
@@ -231,6 +218,8 @@ Item {
|
||||
sourceComponent: Component {
|
||||
SpotlightContextMenu {
|
||||
appLauncher: spotlightKeyHandler.appLauncher
|
||||
parentHandler: spotlightKeyHandler
|
||||
parentModal: spotlightKeyHandler.parentModal
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,12 +280,6 @@ Item {
|
||||
updateSearchMode();
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
const menu = spotlightKeyHandler.usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item;
|
||||
if (menu?.visible) {
|
||||
menu.handleKey(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (parentModal)
|
||||
parentModal.hide();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Spotlight
|
||||
|
||||
@@ -10,66 +11,54 @@ PanelWindow {
|
||||
WlrLayershell.namespace: "dms:spotlight-context-menu"
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
property var appLauncher: null
|
||||
property var parentHandler: null
|
||||
property var parentModal: null
|
||||
property real menuPositionX: 0
|
||||
property real menuPositionY: 0
|
||||
|
||||
|
||||
readonly property real shadowBuffer: 5
|
||||
|
||||
screen: DankModalWindow.targetScreen
|
||||
|
||||
screen: parentModal?.effectiveScreen
|
||||
|
||||
function show(x, y, app, fromKeyboard) {
|
||||
fromKeyboard = fromKeyboard || false;
|
||||
menuContent.currentApp = app;
|
||||
|
||||
|
||||
let screenX = x;
|
||||
let screenY = y;
|
||||
|
||||
const modalX = DankModalWindow.modalX;
|
||||
const modalY = DankModalWindow.modalY;
|
||||
|
||||
if (fromKeyboard) {
|
||||
screenX = x + modalX;
|
||||
screenY = y + modalY;
|
||||
} else {
|
||||
screenX = x + (modalX - shadowBuffer);
|
||||
screenY = y + (modalY - shadowBuffer);
|
||||
|
||||
if (parentModal) {
|
||||
if (fromKeyboard) {
|
||||
screenX = x + parentModal.alignedX;
|
||||
screenY = y + parentModal.alignedY;
|
||||
} else {
|
||||
screenX = x + (parentModal.alignedX - shadowBuffer);
|
||||
screenY = y + (parentModal.alignedY - shadowBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
menuPositionX = screenX;
|
||||
menuPositionY = screenY;
|
||||
|
||||
|
||||
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
|
||||
menuContent.keyboardNavigation = true;
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function handleKey(event) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Down:
|
||||
menuContent.selectNext();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Up:
|
||||
menuContent.selectPrevious();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
menuContent.activateSelected();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Escape:
|
||||
case Qt.Key_Menu:
|
||||
hide();
|
||||
event.accepted = true;
|
||||
break;
|
||||
|
||||
if (parentHandler) {
|
||||
parentHandler.enabled = false;
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
menuContent.keyboardHandler.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (parentHandler) {
|
||||
parentHandler.enabled = true;
|
||||
}
|
||||
visible = false;
|
||||
}
|
||||
|
||||
@@ -82,6 +71,11 @@ PanelWindow {
|
||||
bottom: true
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible && parentHandler) {
|
||||
parentHandler.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
SpotlightContextMenuContent {
|
||||
id: menuContent
|
||||
|
||||
@@ -8,57 +8,51 @@ Popup {
|
||||
id: root
|
||||
|
||||
property var appLauncher: null
|
||||
property var parentHandler: null
|
||||
property var searchField: null
|
||||
|
||||
function show(x, y, app, fromKeyboard) {
|
||||
fromKeyboard = fromKeyboard || false;
|
||||
menuContent.currentApp = app;
|
||||
|
||||
|
||||
root.x = x + 4;
|
||||
root.y = y + 4;
|
||||
|
||||
|
||||
menuContent.selectedMenuIndex = fromKeyboard ? 0 : -1;
|
||||
menuContent.keyboardNavigation = true;
|
||||
|
||||
|
||||
if (parentHandler) {
|
||||
parentHandler.enabled = false;
|
||||
}
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
function handleKey(event) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Down:
|
||||
menuContent.selectNext();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Up:
|
||||
menuContent.selectPrevious();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
menuContent.activateSelected();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Escape:
|
||||
case Qt.Key_Menu:
|
||||
hide();
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
Qt.callLater(() => {
|
||||
menuContent.keyboardHandler.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (parentHandler) {
|
||||
parentHandler.enabled = true;
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
width: menuContent.implicitWidth
|
||||
height: menuContent.implicitHeight
|
||||
padding: 0
|
||||
closePolicy: Popup.CloseOnPressOutside
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
modal: true
|
||||
dim: false
|
||||
background: Item {}
|
||||
|
||||
onClosed: {
|
||||
if (parentHandler) {
|
||||
parentHandler.enabled = true;
|
||||
}
|
||||
if (searchField) {
|
||||
Qt.callLater(() => {
|
||||
searchField.forceActiveFocus();
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
|
||||
DankModal {
|
||||
id: spotlightModal
|
||||
|
||||
layerNamespace: "dms:spotlight"
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [spotlightModal.contentWindow]
|
||||
active: spotlightModal.useHyprlandFocusGrab && spotlightModal.shouldHaveFocus
|
||||
}
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property alias spotlightContent: spotlightContentInstance
|
||||
property bool openedFromOverview: false
|
||||
@@ -16,18 +23,32 @@ DankModal {
|
||||
openedFromOverview = false;
|
||||
spotlightOpen = true;
|
||||
open();
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher)
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = query;
|
||||
if (spotlightContent.searchField)
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
}
|
||||
}
|
||||
|
||||
spotlightOpen = true;
|
||||
open();
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent && spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -36,24 +57,23 @@ DankModal {
|
||||
close();
|
||||
}
|
||||
|
||||
function onFullyClosed() {
|
||||
resetContent();
|
||||
}
|
||||
|
||||
function resetContent() {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = "";
|
||||
spotlightContent.appLauncher.selectedIndex = 0;
|
||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"));
|
||||
onDialogClosed: {
|
||||
if (spotlightContent) {
|
||||
if (spotlightContent.appLauncher) {
|
||||
spotlightContent.appLauncher.searchQuery = "";
|
||||
spotlightContent.appLauncher.selectedIndex = 0;
|
||||
spotlightContent.appLauncher.setCategory(I18n.tr("All"));
|
||||
}
|
||||
if (spotlightContent.fileSearchController) {
|
||||
spotlightContent.fileSearchController.reset();
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
}
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = "";
|
||||
}
|
||||
}
|
||||
if (spotlightContent.fileSearchController)
|
||||
spotlightContent.fileSearchController.reset();
|
||||
if (spotlightContent.resetScroll)
|
||||
spotlightContent.resetScroll();
|
||||
if (spotlightContent.searchField)
|
||||
spotlightContent.searchField.text = "";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -78,10 +98,16 @@ DankModal {
|
||||
animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
onVisibleChanged: () => {
|
||||
if (!visible)
|
||||
return;
|
||||
if (!spotlightOpen)
|
||||
if (visible && !spotlightOpen) {
|
||||
show();
|
||||
}
|
||||
if (visible && spotlightContent) {
|
||||
Qt.callLater(() => {
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
@@ -130,10 +156,6 @@ DankModal {
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
DankModalWindow.persistentModal = spotlightModal;
|
||||
}
|
||||
|
||||
SpotlightContent {
|
||||
id: spotlightContentInstance
|
||||
|
||||
|
||||
@@ -51,6 +51,33 @@ Rectangle {
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 32
|
||||
z: 100
|
||||
visible: {
|
||||
if (!appLauncher)
|
||||
return false;
|
||||
const view = appLauncher.viewMode === "list" ? resultsList : (gridLoader.item || resultsList);
|
||||
const isLastItem = appLauncher.viewMode === "list" ? view.currentIndex >= view.count - 1 : (gridLoader.item ? Math.floor(view.currentIndex / view.actualColumns) >= Math.floor((view.count - 1) / view.actualColumns) : false);
|
||||
const hasOverflow = view.contentHeight > view.height;
|
||||
const atBottom = view.contentY >= view.contentHeight - view.height - 1;
|
||||
return hasOverflow && (!isLastItem || !atBottom);
|
||||
}
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: resultsList
|
||||
|
||||
@@ -70,14 +97,19 @@ Rectangle {
|
||||
return;
|
||||
const itemY = index * (itemHeight + itemSpacing);
|
||||
const itemBottom = itemY + itemHeight;
|
||||
const fadeHeight = 32;
|
||||
const isLastItem = index === count - 1;
|
||||
if (itemY < contentY)
|
||||
contentY = itemY;
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height;
|
||||
else if (itemBottom > contentY + height - (isLastItem ? 0 : fadeHeight))
|
||||
contentY = Math.min(itemBottom - height + (isLastItem ? 0 : fadeHeight), contentHeight - height);
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: 1
|
||||
visible: appLauncher && appLauncher.viewMode === "list"
|
||||
model: appLauncher ? appLauncher.model : null
|
||||
currentIndex: appLauncher ? appLauncher.selectedIndex : -1
|
||||
@@ -127,7 +159,10 @@ Rectangle {
|
||||
property real _lastWidth: 0
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: 1
|
||||
visible: appLauncher && appLauncher.viewMode === "grid"
|
||||
active: appLauncher && appLauncher.viewMode === "grid"
|
||||
asynchronous: false
|
||||
@@ -177,10 +212,12 @@ Rectangle {
|
||||
return;
|
||||
const itemY = Math.floor(index / actualColumns) * cellHeight;
|
||||
const itemBottom = itemY + cellHeight;
|
||||
const fadeHeight = 32;
|
||||
const isLastRow = Math.floor(index / actualColumns) >= Math.floor((count - 1) / actualColumns);
|
||||
if (itemY < contentY)
|
||||
contentY = itemY;
|
||||
else if (itemBottom > contentY + height)
|
||||
contentY = itemBottom - height;
|
||||
else if (itemBottom > contentY + height - (isLastRow ? 0 : fadeHeight))
|
||||
contentY = Math.min(itemBottom - height + (isLastRow ? 0 : fadeHeight), contentHeight - height);
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user