mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
Compare commits
86 Commits
f236706d6a
...
marcus/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fad2826b1 | ||
|
|
57ee0fb2bd | ||
|
|
3ef10e73a5 | ||
|
|
dc40492fc7 | ||
|
|
e606a76a86 | ||
|
|
8838fd67b9 | ||
|
|
c570e20308 | ||
|
|
0a00ef39e3 | ||
|
|
9a08b81214 | ||
|
|
c617ae26a2 | ||
|
|
f6a776a692 | ||
|
|
54b253099d | ||
|
|
f662aca58c | ||
|
|
76e7755496 | ||
|
|
e05ad81c13 | ||
|
|
cffb16d7f7 | ||
|
|
18ca571944 | ||
|
|
3ae1973e21 | ||
|
|
308c8c3ea7 | ||
|
|
f49b5dd037 | ||
|
|
f245ba82ad | ||
|
|
60d22d6973 | ||
|
|
d6f48a82d9 | ||
|
|
c0d73dae67 | ||
|
|
49eb60589d | ||
|
|
89993b7421 | ||
|
|
511cb93806 | ||
|
|
8ce78e7134 | ||
|
|
9ebfab2e78 | ||
|
|
833d245251 | ||
|
|
00d3024143 | ||
|
|
aedeab8a6a | ||
|
|
4d39169eb8 | ||
|
|
2ddc448150 | ||
|
|
f9a6b4ce2c | ||
|
|
22b2b69413 | ||
|
|
7f11632ea6 | ||
|
|
c0b4d5e2c2 | ||
|
|
2c23d0249c | ||
|
|
c3233fbf61 | ||
|
|
ecfc8e208c | ||
|
|
52d5e21fc4 | ||
|
|
6d0c56554f | ||
|
|
844e91dc9e | ||
|
|
1f00b5f577 | ||
|
|
2c48458384 | ||
|
|
ddda87c5a7 | ||
|
|
6b1bbca620 | ||
|
|
b5378e5d3c | ||
|
|
c69a55df29 | ||
|
|
5faa1a993a | ||
|
|
e56481f6d7 | ||
|
|
f9610d457c | ||
|
|
ae066f42a4 | ||
|
|
c60dd42fa7 | ||
|
|
7aac5ac5a1 | ||
|
|
ad0f3fa33b | ||
|
|
63d121b796 | ||
|
|
4291cfe82f | ||
|
|
f312868154 | ||
|
|
5b42d34ac8 | ||
|
|
397a8c275d | ||
|
|
2aabee453b | ||
|
|
185333a615 | ||
|
|
7d177eb1d4 | ||
|
|
705a84051d | ||
|
|
f6821f80e1 | ||
|
|
e7a6f5228d | ||
|
|
8161fd6acb | ||
|
|
2137920e81 | ||
|
|
879102599c | ||
|
|
44190f07fe | ||
|
|
a41487eb8f | ||
|
|
e1acaaa27c | ||
|
|
08a97aeff8 | ||
|
|
5b7302b46d | ||
|
|
34c0bba130 | ||
|
|
5a53447272 | ||
|
|
b6847289ff | ||
|
|
d22c43e08b | ||
|
|
d9deaa8d74 | ||
|
|
6c7776a9a6 | ||
|
|
62bd6e41ef | ||
|
|
293c7b42c6 | ||
|
|
788da62777 | ||
|
|
2c7f24a913 |
@@ -37,7 +37,10 @@ if [[ -n "$STAGED_CORE_FILES" ]]; then
|
|||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
echo " Running tests..."
|
echo " Running tests..."
|
||||||
go test ./... >/dev/null
|
if ! go test ./... >/dev/null 2>&1; then
|
||||||
|
echo "Tests failed! Run 'go test ./...' for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Build checks
|
# Build checks
|
||||||
echo " Building..."
|
echo " Building..."
|
||||||
|
|||||||
30
.github/workflows/nix-pr-check.yml
vendored
Normal file
30
.github/workflows/nix-pr-check.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Check nix flake
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
paths:
|
||||||
|
- "flake.*"
|
||||||
|
- "distro/nix/**"
|
||||||
|
jobs:
|
||||||
|
check-flake:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
- name: Update vendorHash in flake.nix
|
||||||
|
run: nix flake check
|
||||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -517,7 +517,6 @@ jobs:
|
|||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
Recommends: cliphist
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: hyprpicker
|
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: wl-clipboard
|
Recommends: wl-clipboard
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
|
|||||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -102,39 +102,6 @@ go.work.sum
|
|||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
|
||||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
|
||||||
#
|
|
||||||
# Binaries for programs and plugins
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Code coverage profiles and other test artifacts
|
|
||||||
*.out
|
|
||||||
coverage.*
|
|
||||||
*.coverprofile
|
|
||||||
profile.cov
|
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
# vendor/
|
|
||||||
|
|
||||||
# Go workspace file
|
|
||||||
go.work
|
|
||||||
go.work.sum
|
|
||||||
|
|
||||||
# env file
|
|
||||||
.env
|
|
||||||
|
|
||||||
# Editor/IDE
|
|
||||||
# .idea/
|
|
||||||
# .vscode/
|
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
# Extracted source trees in Ubuntu package directories
|
# Extracted source trees in Ubuntu package directories
|
||||||
@@ -142,3 +109,7 @@ distro/ubuntu/*/dms-git-repo/
|
|||||||
distro/ubuntu/*/DankMaterialShell-*/
|
distro/ubuntu/*/DankMaterialShell-*/
|
||||||
distro/ubuntu/danklinux/*/dsearch-*/
|
distro/ubuntu/danklinux/*/dsearch-*/
|
||||||
distro/ubuntu/danklinux/*/dgop-*/
|
distro/ubuntu/danklinux/*/dgop-*/
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.envrc
|
||||||
|
.direnv/
|
||||||
|
|||||||
@@ -12,6 +12,21 @@ Enable pre-commit hooks to catch CI failures before pushing:
|
|||||||
git config core.hooksPath .githooks
|
git config core.hooksPath .githooks
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Nix Development Shell
|
||||||
|
|
||||||
|
If you have Nix installed with flakes enabled, you can use the provided development shell which includes all necessary dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix develop
|
||||||
|
```
|
||||||
|
|
||||||
|
This will provide:
|
||||||
|
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||||
|
- Quickshell and required QML packages
|
||||||
|
- Properly configured QML2_IMPORT_PATH
|
||||||
|
|
||||||
|
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
||||||
|
|
||||||
## VSCode Setup
|
## VSCode Setup
|
||||||
|
|
||||||
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ Exec=dms open %u
|
|||||||
Icon=danklogo
|
Icon=danklogo
|
||||||
Terminal=false
|
Terminal=false
|
||||||
NoDisplay=true
|
NoDisplay=true
|
||||||
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/file;text/html;
|
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ linters:
|
|||||||
# Signal handling
|
# Signal handling
|
||||||
- (*os.Process).Signal
|
- (*os.Process).Signal
|
||||||
- (*os.Process).Kill
|
- (*os.Process).Kill
|
||||||
|
- syscall.Kill
|
||||||
# DBus cleanup
|
# DBus cleanup
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
|
|
||||||
**Wayland Protocols**
|
**Wayland Protocols**
|
||||||
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
||||||
|
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
|
||||||
|
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
|
||||||
|
- `wp-viewporter` - Fractional scaling support
|
||||||
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
||||||
- `ext-workspace-v1` - Workspace protocol support
|
- `ext-workspace-v1` - Workspace protocol support
|
||||||
- `wlr-output-management-unstable-v1` - Display configuration
|
- `wlr-output-management-unstable-v1` - Display configuration
|
||||||
@@ -44,9 +47,24 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
||||||
- `dms plugins [install|browse|search]` - Plugin management
|
- `dms plugins [install|browse|search]` - Plugin management
|
||||||
- `dms brightness [list|set]` - Control display/monitor brightness
|
- `dms brightness [list|set]` - Control display/monitor brightness
|
||||||
|
- `dms color pick` - Native color picker (see below)
|
||||||
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
||||||
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
||||||
|
|
||||||
|
### Color Picker
|
||||||
|
|
||||||
|
Native Wayland color picker with magnifier, no external dependencies. Supports HiDPI and fractional scaling.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dms color pick # Pick color, output hex
|
||||||
|
dms color pick --rgb # Output as RGB (255 128 64)
|
||||||
|
dms color pick --hsv # Output as HSV (24 75% 100%)
|
||||||
|
dms color pick --json # Output all formats as JSON
|
||||||
|
dms color pick -a # Auto-copy to clipboard
|
||||||
|
```
|
||||||
|
|
||||||
|
The on-screen preview displays the selected format. JSON output includes hex, RGB, HSL, HSV, and CMYK values.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Requires Go 1.24+
|
Requires Go 1.24+
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
@@ -454,7 +454,6 @@ func uninstallPluginCLI(idOrName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCommonCommands returns the commands available in all builds
|
|
||||||
func getCommonCommands() []*cobra.Command {
|
func getCommonCommands() []*cobra.Command {
|
||||||
return []*cobra.Command{
|
return []*cobra.Command{
|
||||||
versionCmd,
|
versionCmd,
|
||||||
@@ -471,5 +470,9 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
keybindsCmd,
|
keybindsCmd,
|
||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
|
colorCmd,
|
||||||
|
screenshotCmd,
|
||||||
|
notifyActionCmd,
|
||||||
|
matugenCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var dank16Cmd = &cobra.Command{
|
var dank16Cmd = &cobra.Command{
|
||||||
Use: "dank16 <hex_color>",
|
Use: "dank16 [hex_color]",
|
||||||
Short: "Generate Base16 color palettes",
|
Short: "Generate Base16 color palettes",
|
||||||
Long: "Generate Base16 color palettes from a color with support for various output formats",
|
Long: "Generate Base16 color palettes from a color with support for various output formats",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
Run: runDank16,
|
Run: runDank16,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant")
|
dank16Cmd.Flags().Bool("light", false, "Generate light theme variant (sets default to light)")
|
||||||
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
||||||
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||||
@@ -27,17 +27,15 @@ func init() {
|
|||||||
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||||
dank16Cmd.Flags().String("background", "", "Custom background color")
|
dank16Cmd.Flags().String("background", "", "Custom background color")
|
||||||
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag")
|
||||||
|
dank16Cmd.Flags().Bool("variants", false, "Output all variants (dark/light/default) in JSON")
|
||||||
|
dank16Cmd.Flags().String("primary-dark", "", "Primary color for dark mode (use with --variants)")
|
||||||
|
dank16Cmd.Flags().String("primary-light", "", "Primary color for light mode (use with --variants)")
|
||||||
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
_ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDank16(cmd *cobra.Command, args []string) {
|
func runDank16(cmd *cobra.Command, args []string) {
|
||||||
primaryColor := args[0]
|
|
||||||
if !strings.HasPrefix(primaryColor, "#") {
|
|
||||||
primaryColor = "#" + primaryColor
|
|
||||||
}
|
|
||||||
|
|
||||||
isLight, _ := cmd.Flags().GetBool("light")
|
isLight, _ := cmd.Flags().GetBool("light")
|
||||||
isJson, _ := cmd.Flags().GetBool("json")
|
isJson, _ := cmd.Flags().GetBool("json")
|
||||||
isKitty, _ := cmd.Flags().GetBool("kitty")
|
isKitty, _ := cmd.Flags().GetBool("kitty")
|
||||||
@@ -47,16 +45,57 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||||
background, _ := cmd.Flags().GetString("background")
|
background, _ := cmd.Flags().GetString("background")
|
||||||
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
contrastAlgo, _ := cmd.Flags().GetString("contrast")
|
||||||
|
useVariants, _ := cmd.Flags().GetBool("variants")
|
||||||
|
primaryDark, _ := cmd.Flags().GetString("primary-dark")
|
||||||
|
primaryLight, _ := cmd.Flags().GetString("primary-light")
|
||||||
|
|
||||||
if background != "" && !strings.HasPrefix(background, "#") {
|
if background != "" && !strings.HasPrefix(background, "#") {
|
||||||
background = "#" + background
|
background = "#" + background
|
||||||
}
|
}
|
||||||
|
if primaryDark != "" && !strings.HasPrefix(primaryDark, "#") {
|
||||||
|
primaryDark = "#" + primaryDark
|
||||||
|
}
|
||||||
|
if primaryLight != "" && !strings.HasPrefix(primaryLight, "#") {
|
||||||
|
primaryLight = "#" + primaryLight
|
||||||
|
}
|
||||||
|
|
||||||
contrastAlgo = strings.ToLower(contrastAlgo)
|
contrastAlgo = strings.ToLower(contrastAlgo)
|
||||||
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
|
if contrastAlgo != "dps" && contrastAlgo != "wcag" {
|
||||||
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
|
log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if useVariants {
|
||||||
|
if primaryDark == "" || primaryLight == "" {
|
||||||
|
if len(args) == 0 {
|
||||||
|
log.Fatalf("--variants requires either a positional color argument or both --primary-dark and --primary-light")
|
||||||
|
}
|
||||||
|
primaryColor := args[0]
|
||||||
|
if !strings.HasPrefix(primaryColor, "#") {
|
||||||
|
primaryColor = "#" + primaryColor
|
||||||
|
}
|
||||||
|
primaryDark = primaryColor
|
||||||
|
primaryLight = primaryColor
|
||||||
|
}
|
||||||
|
variantOpts := dank16.VariantOptions{
|
||||||
|
PrimaryDark: primaryDark,
|
||||||
|
PrimaryLight: primaryLight,
|
||||||
|
Background: background,
|
||||||
|
UseDPS: contrastAlgo == "dps",
|
||||||
|
IsLightMode: isLight,
|
||||||
|
}
|
||||||
|
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||||
|
fmt.Print(dank16.GenerateVariantJSON(variantColors))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
log.Fatalf("A color argument is required (or use --variants with --primary-dark and --primary-light)")
|
||||||
|
}
|
||||||
|
primaryColor := args[0]
|
||||||
|
if !strings.HasPrefix(primaryColor, "#") {
|
||||||
|
primaryColor = "#" + primaryColor
|
||||||
|
}
|
||||||
|
|
||||||
opts := dank16.PaletteOptions{
|
opts := dank16.PaletteOptions{
|
||||||
IsLight: isLight,
|
IsLight: isLight,
|
||||||
Background: background,
|
Background: background,
|
||||||
|
|||||||
182
core/cmd/dms/commands_matugen.go
Normal file
182
core/cmd/dms/commands_matugen.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var matugenCmd = &cobra.Command{
|
||||||
|
Use: "matugen",
|
||||||
|
Short: "Generate Material Design themes",
|
||||||
|
Long: "Generate Material Design themes using matugen with dank16 color integration",
|
||||||
|
}
|
||||||
|
|
||||||
|
var matugenGenerateCmd = &cobra.Command{
|
||||||
|
Use: "generate",
|
||||||
|
Short: "Generate theme synchronously",
|
||||||
|
Run: runMatugenGenerate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var matugenQueueCmd = &cobra.Command{
|
||||||
|
Use: "queue",
|
||||||
|
Short: "Queue theme generation (uses socket if available)",
|
||||||
|
Run: runMatugenQueue,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||||
|
matugenCmd.AddCommand(matugenQueueCmd)
|
||||||
|
|
||||||
|
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
||||||
|
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
||||||
|
cmd.Flags().String("shell-dir", "", "DMS shell installation directory")
|
||||||
|
cmd.Flags().String("config-dir", "", "User config directory")
|
||||||
|
cmd.Flags().String("kind", "image", "Source type: image or hex")
|
||||||
|
cmd.Flags().String("value", "", "Wallpaper path or hex color")
|
||||||
|
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
|
||||||
|
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
|
||||||
|
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
|
||||||
|
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
|
||||||
|
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
||||||
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
|
}
|
||||||
|
|
||||||
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
|
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
|
stateDir, _ := cmd.Flags().GetString("state-dir")
|
||||||
|
shellDir, _ := cmd.Flags().GetString("shell-dir")
|
||||||
|
configDir, _ := cmd.Flags().GetString("config-dir")
|
||||||
|
kind, _ := cmd.Flags().GetString("kind")
|
||||||
|
value, _ := cmd.Flags().GetString("value")
|
||||||
|
mode, _ := cmd.Flags().GetString("mode")
|
||||||
|
iconTheme, _ := cmd.Flags().GetString("icon-theme")
|
||||||
|
matugenType, _ := cmd.Flags().GetString("matugen-type")
|
||||||
|
runUserTemplates, _ := cmd.Flags().GetBool("run-user-templates")
|
||||||
|
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
||||||
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
|
|
||||||
|
return matugen.Options{
|
||||||
|
StateDir: stateDir,
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: configDir,
|
||||||
|
Kind: kind,
|
||||||
|
Value: value,
|
||||||
|
Mode: mode,
|
||||||
|
IconTheme: iconTheme,
|
||||||
|
MatugenType: matugenType,
|
||||||
|
RunUserTemplates: runUserTemplates,
|
||||||
|
StockColors: stockColors,
|
||||||
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
|
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||||
|
opts := buildMatugenOptions(cmd)
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||||
|
opts := buildMatugenOptions(cmd)
|
||||||
|
wait, _ := cmd.Flags().GetBool("wait")
|
||||||
|
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||||
|
|
||||||
|
socketPath := os.Getenv("DMS_SOCKET")
|
||||||
|
if socketPath == "" {
|
||||||
|
var err error
|
||||||
|
socketPath, err = server.FindSocket()
|
||||||
|
if err != nil {
|
||||||
|
log.Info("No socket available, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Socket connection failed, running synchronously")
|
||||||
|
if err := matugen.Run(opts); err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
request := map[string]any{
|
||||||
|
"id": 1,
|
||||||
|
"method": "matugen.queue",
|
||||||
|
"params": map[string]any{
|
||||||
|
"stateDir": opts.StateDir,
|
||||||
|
"shellDir": opts.ShellDir,
|
||||||
|
"configDir": opts.ConfigDir,
|
||||||
|
"kind": opts.Kind,
|
||||||
|
"value": opts.Value,
|
||||||
|
"mode": opts.Mode,
|
||||||
|
"iconTheme": opts.IconTheme,
|
||||||
|
"matugenType": opts.MatugenType,
|
||||||
|
"runUserTemplates": opts.RunUserTemplates,
|
||||||
|
"stockColors": opts.StockColors,
|
||||||
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
|
"wait": wait,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(conn).Encode(request); err != nil {
|
||||||
|
log.Fatalf("Failed to send request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wait {
|
||||||
|
fmt.Println("Theme generation queued")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
resultCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
var response struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Result any `json:"result"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(conn).Decode(&response); err != nil {
|
||||||
|
resultCh <- fmt.Errorf("failed to read response: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if response.Error != "" {
|
||||||
|
resultCh <- fmt.Errorf("server error: %s", response.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultCh <- nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-resultCh:
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Theme generation completed")
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Fatalf("Timeout waiting for theme generation")
|
||||||
|
}
|
||||||
|
}
|
||||||
376
core/cmd/dms/commands_screenshot.go
Normal file
376
core/cmd/dms/commands_screenshot.go
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ssOutputName string
|
||||||
|
ssIncludeCursor bool
|
||||||
|
ssFormat string
|
||||||
|
ssQuality int
|
||||||
|
ssOutputDir string
|
||||||
|
ssFilename string
|
||||||
|
ssNoClipboard bool
|
||||||
|
ssNoFile bool
|
||||||
|
ssNoNotify bool
|
||||||
|
ssStdout bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var screenshotCmd = &cobra.Command{
|
||||||
|
Use: "screenshot",
|
||||||
|
Short: "Capture screenshots",
|
||||||
|
Long: `Capture screenshots from Wayland displays.
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
region - Select a region interactively (default)
|
||||||
|
full - Capture the focused output
|
||||||
|
all - Capture all outputs combined
|
||||||
|
output - Capture a specific output by name
|
||||||
|
window - Capture the focused window (Hyprland/DWL)
|
||||||
|
last - Capture the last selected region
|
||||||
|
|
||||||
|
Output format (--format):
|
||||||
|
png - PNG format (default)
|
||||||
|
jpg/jpeg - JPEG format
|
||||||
|
ppm - PPM format
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms screenshot # Region select, save file + clipboard
|
||||||
|
dms screenshot full # Full screen of focused output
|
||||||
|
dms screenshot all # All screens combined
|
||||||
|
dms screenshot output -o DP-1 # Specific output
|
||||||
|
dms screenshot window # Focused window (Hyprland)
|
||||||
|
dms screenshot last # Last region (pre-selected)
|
||||||
|
dms screenshot --no-clipboard # Save file only
|
||||||
|
dms screenshot --no-file # Clipboard only
|
||||||
|
dms screenshot --cursor # Include cursor
|
||||||
|
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssRegionCmd = &cobra.Command{
|
||||||
|
Use: "region",
|
||||||
|
Short: "Select a region interactively",
|
||||||
|
Run: runScreenshotRegion,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssFullCmd = &cobra.Command{
|
||||||
|
Use: "full",
|
||||||
|
Short: "Capture the focused output",
|
||||||
|
Run: runScreenshotFull,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssAllCmd = &cobra.Command{
|
||||||
|
Use: "all",
|
||||||
|
Short: "Capture all outputs combined",
|
||||||
|
Run: runScreenshotAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssOutputCmd = &cobra.Command{
|
||||||
|
Use: "output",
|
||||||
|
Short: "Capture a specific output",
|
||||||
|
Run: runScreenshotOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssLastCmd = &cobra.Command{
|
||||||
|
Use: "last",
|
||||||
|
Short: "Capture the last selected region",
|
||||||
|
Long: `Capture the previously selected region without interactive selection.
|
||||||
|
If no previous region exists, falls back to interactive selection.`,
|
||||||
|
Run: runScreenshotLast,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssWindowCmd = &cobra.Command{
|
||||||
|
Use: "window",
|
||||||
|
Short: "Capture the focused window",
|
||||||
|
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
|
||||||
|
Run: runScreenshotWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ssListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List available outputs",
|
||||||
|
Run: runScreenshotList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var notifyActionCmd = &cobra.Command{
|
||||||
|
Use: "notify-action",
|
||||||
|
Hidden: true,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
screenshot.RunNotifyActionListener(args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||||
|
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||||
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||||
|
screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
|
||||||
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
|
screenshotCmd.AddCommand(ssFullCmd)
|
||||||
|
screenshotCmd.AddCommand(ssAllCmd)
|
||||||
|
screenshotCmd.AddCommand(ssOutputCmd)
|
||||||
|
screenshotCmd.AddCommand(ssLastCmd)
|
||||||
|
screenshotCmd.AddCommand(ssWindowCmd)
|
||||||
|
screenshotCmd.AddCommand(ssListCmd)
|
||||||
|
|
||||||
|
screenshotCmd.Run = runScreenshotRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||||
|
config := screenshot.DefaultConfig()
|
||||||
|
config.Mode = mode
|
||||||
|
config.OutputName = ssOutputName
|
||||||
|
config.IncludeCursor = ssIncludeCursor
|
||||||
|
config.Clipboard = !ssNoClipboard
|
||||||
|
config.SaveFile = !ssNoFile
|
||||||
|
config.Notify = !ssNoNotify
|
||||||
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
|
if ssOutputDir != "" {
|
||||||
|
config.OutputDir = ssOutputDir
|
||||||
|
}
|
||||||
|
if ssFilename != "" {
|
||||||
|
config.Filename = ssFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(ssFormat) {
|
||||||
|
case "jpg", "jpeg":
|
||||||
|
config.Format = screenshot.FormatJPEG
|
||||||
|
case "ppm":
|
||||||
|
config.Format = screenshot.FormatPPM
|
||||||
|
default:
|
||||||
|
config.Format = screenshot.FormatPNG
|
||||||
|
}
|
||||||
|
|
||||||
|
if ssQuality < 1 {
|
||||||
|
ssQuality = 1
|
||||||
|
}
|
||||||
|
if ssQuality > 100 {
|
||||||
|
ssQuality = 100
|
||||||
|
}
|
||||||
|
config.Quality = ssQuality
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshot(config screenshot.Config) {
|
||||||
|
sc := screenshot.New(config)
|
||||||
|
result, err := sc.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == nil {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer result.Buffer.Close()
|
||||||
|
|
||||||
|
if result.YInverted {
|
||||||
|
result.Buffer.FlipVertical()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Stdout {
|
||||||
|
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath string
|
||||||
|
|
||||||
|
if config.SaveFile {
|
||||||
|
outputDir := config.OutputDir
|
||||||
|
if outputDir == "" {
|
||||||
|
outputDir = screenshot.GetOutputDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := config.Filename
|
||||||
|
if filename == "" {
|
||||||
|
filename = screenshot.GenerateFilename(config.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = filepath.Join(outputDir, filename)
|
||||||
|
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Clipboard {
|
||||||
|
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !config.SaveFile {
|
||||||
|
fmt.Println("Copied to clipboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Notify {
|
||||||
|
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
||||||
|
screenshot.SendNotification(screenshot.NotifyResult{
|
||||||
|
FilePath: filePath,
|
||||||
|
Clipboard: config.Clipboard,
|
||||||
|
ImageData: thumbData,
|
||||||
|
Width: thumbW,
|
||||||
|
Height: thumbH,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
var mimeType string
|
||||||
|
var data bytes.Buffer
|
||||||
|
|
||||||
|
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
mimeType = "image/png"
|
||||||
|
if err := screenshot.EncodePNG(&data, img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("wl-copy", "--type", mimeType)
|
||||||
|
cmd.Stdin = &data
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
||||||
|
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
return screenshot.EncodeJPEG(os.Stdout, img, quality)
|
||||||
|
default:
|
||||||
|
return screenshot.EncodePNG(os.Stdout, img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) {
|
||||||
|
srcW, srcH := buf.Width, buf.Height
|
||||||
|
scale := 1.0
|
||||||
|
if srcW > maxSize || srcH > maxSize {
|
||||||
|
if srcW > srcH {
|
||||||
|
scale = float64(maxSize) / float64(srcW)
|
||||||
|
} else {
|
||||||
|
scale = float64(maxSize) / float64(srcH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dstW := int(float64(srcW) * scale)
|
||||||
|
dstH := int(float64(srcH) * scale)
|
||||||
|
if dstW < 1 {
|
||||||
|
dstW = 1
|
||||||
|
}
|
||||||
|
if dstH < 1 {
|
||||||
|
dstH = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf.Data()
|
||||||
|
rgb := make([]byte, dstW*dstH*3)
|
||||||
|
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
|
||||||
|
|
||||||
|
for y := 0; y < dstH; y++ {
|
||||||
|
srcY := int(float64(y) / scale)
|
||||||
|
if srcY >= srcH {
|
||||||
|
srcY = srcH - 1
|
||||||
|
}
|
||||||
|
for x := 0; x < dstW; x++ {
|
||||||
|
srcX := int(float64(x) / scale)
|
||||||
|
if srcX >= srcW {
|
||||||
|
srcX = srcW - 1
|
||||||
|
}
|
||||||
|
si := srcY*buf.Stride + srcX*4
|
||||||
|
di := (y*dstW + x) * 3
|
||||||
|
if si+2 < len(data) {
|
||||||
|
if swapRB {
|
||||||
|
rgb[di+0] = data[si+2]
|
||||||
|
rgb[di+1] = data[si+1]
|
||||||
|
rgb[di+2] = data[si+0]
|
||||||
|
} else {
|
||||||
|
rgb[di+0] = data[si+0]
|
||||||
|
rgb[di+1] = data[si+1]
|
||||||
|
rgb[di+2] = data[si+2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgb, dstW, dstH
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotRegion(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeRegion)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotAll(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeAllScreens)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotOutput(cmd *cobra.Command, args []string) {
|
||||||
|
if ssOutputName == "" && len(args) > 0 {
|
||||||
|
ssOutputName = args[0]
|
||||||
|
}
|
||||||
|
if ssOutputName == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
config := getScreenshotConfig(screenshot.ModeOutput)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotLast(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeLastRegion)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotWindow(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeWindow)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScreenshotList(cmd *cobra.Command, args []string) {
|
||||||
|
outputs, err := screenshot.ListOutputs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range outputs {
|
||||||
|
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
|
||||||
|
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ipcTargets map[string][]string
|
type ipcTargets map[string]map[string][]string
|
||||||
|
|
||||||
var isSessionManaged bool
|
var isSessionManaged bool
|
||||||
|
|
||||||
@@ -476,28 +476,40 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||||
targets := map[string][]string{}
|
targets := make(ipcTargets)
|
||||||
var currentTarget string
|
var currentTarget string
|
||||||
for _, line := range strings.Split(output, "\n") {
|
for _, line := range strings.Split(output, "\n") {
|
||||||
if strings.HasPrefix(line, "target ") {
|
if strings.HasPrefix(line, "target ") {
|
||||||
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
||||||
|
targets[currentTarget] = make(map[string][]string)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
||||||
|
argsList := []string{}
|
||||||
currentFunc := strings.TrimPrefix(line, " function ")
|
currentFunc := strings.TrimPrefix(line, " function ")
|
||||||
currentFunc = strings.SplitN(currentFunc, "(", 2)[0]
|
funcDef := strings.SplitN(currentFunc, "(", 2)
|
||||||
targets[currentTarget] = append(targets[currentTarget], currentFunc)
|
argList := strings.SplitN(funcDef[1], ")", 2)[0]
|
||||||
|
args := strings.Split(argList, ",")
|
||||||
|
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
|
||||||
|
argsList = append(argsList, funcDef[0])
|
||||||
|
for _, arg := range args {
|
||||||
|
argName := strings.SplitN(strings.TrimSpace(arg), ":", 2)[0]
|
||||||
|
argsList = append(argsList, argName)
|
||||||
|
}
|
||||||
|
targets[currentTarget][funcDef[0]] = argsList
|
||||||
|
} else {
|
||||||
|
targets[currentTarget][funcDef[0]] = make([]string, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return targets
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
func getShellIPCCompletions(args []string, toComplete string) []string {
|
func getShellIPCCompletions(args []string, _ string) []string {
|
||||||
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
var targets ipcTargets
|
var targets ipcTargets
|
||||||
|
|
||||||
if output, err := cmd.Output(); err == nil {
|
if output, err := cmd.Output(); err == nil {
|
||||||
log.Debugf("IPC show output: %s", string(output))
|
|
||||||
targets = parseTargetsFromIPCShowOutput(string(output))
|
targets = parseTargetsFromIPCShowOutput(string(output))
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Error getting IPC show output for completions: %v", err)
|
log.Debugf("Error getting IPC show output for completions: %v", err)
|
||||||
@@ -516,8 +528,24 @@ func getShellIPCCompletions(args []string, toComplete string) []string {
|
|||||||
}
|
}
|
||||||
return targetNames
|
return targetNames
|
||||||
}
|
}
|
||||||
|
if len(args) == 1 {
|
||||||
|
if targetFuncs, ok := targets[args[0]]; ok {
|
||||||
|
funcNames := make([]string, 0)
|
||||||
|
for k := range targetFuncs {
|
||||||
|
funcNames = append(funcNames, k)
|
||||||
|
}
|
||||||
|
return funcNames
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(args) <= len(targets[args[0]]) {
|
||||||
|
funcArgs := targets[args[0]][args[1]]
|
||||||
|
if len(funcArgs) >= len(args) {
|
||||||
|
return []string{fmt.Sprintf("[%s]", funcArgs[len(args)-1])}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return targets[args[0]]
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
737
core/internal/colorpicker/picker.go
Normal file
737
core/internal/colorpicker/picker.go
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Format OutputFormat
|
||||||
|
CustomFormat string
|
||||||
|
Lowercase bool
|
||||||
|
Autocopy bool
|
||||||
|
Notify bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Output struct {
|
||||||
|
wlOutput *client.Output
|
||||||
|
name string
|
||||||
|
globalName uint32
|
||||||
|
x, y int32
|
||||||
|
width int32
|
||||||
|
height int32
|
||||||
|
scale int32
|
||||||
|
fractionalScale float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayerSurface struct {
|
||||||
|
output *Output
|
||||||
|
state *SurfaceState
|
||||||
|
wlSurface *client.Surface
|
||||||
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
|
viewport *wp_viewporter.WpViewport
|
||||||
|
wlPool *client.ShmPool
|
||||||
|
wlBuffer *client.Buffer
|
||||||
|
bufferBusy bool
|
||||||
|
oldPool *client.ShmPool
|
||||||
|
oldBuffer *client.Buffer
|
||||||
|
scopyBuffer *client.Buffer
|
||||||
|
configured bool
|
||||||
|
hidden bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Picker struct {
|
||||||
|
config Config
|
||||||
|
|
||||||
|
display *client.Display
|
||||||
|
registry *client.Registry
|
||||||
|
ctx *client.Context
|
||||||
|
|
||||||
|
compositor *client.Compositor
|
||||||
|
shm *client.Shm
|
||||||
|
seat *client.Seat
|
||||||
|
pointer *client.Pointer
|
||||||
|
keyboard *client.Keyboard
|
||||||
|
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||||
|
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||||
|
viewporter *wp_viewporter.WpViewporter
|
||||||
|
|
||||||
|
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
||||||
|
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
||||||
|
|
||||||
|
outputs map[uint32]*Output
|
||||||
|
outputsMu sync.Mutex
|
||||||
|
|
||||||
|
surfaces []*LayerSurface
|
||||||
|
activeSurface *LayerSurface
|
||||||
|
|
||||||
|
running bool
|
||||||
|
pickedColor *Color
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config Config) *Picker {
|
||||||
|
return &Picker{
|
||||||
|
config: config,
|
||||||
|
outputs: make(map[uint32]*Output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) Run() (*Color, error) {
|
||||||
|
if err := p.connect(); err != nil {
|
||||||
|
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer p.cleanup()
|
||||||
|
|
||||||
|
if err := p.setupRegistry(); err != nil {
|
||||||
|
return nil, fmt.Errorf("registry setup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.screencopy == nil {
|
||||||
|
return nil, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.layerShell == nil {
|
||||||
|
return nil, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.seat == nil {
|
||||||
|
return nil, fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra roundtrip to ensure pointer/keyboard from seat capabilities are registered
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip after seat: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.createSurfaces(); err != nil {
|
||||||
|
return nil, fmt.Errorf("create surfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.roundtrip(); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.running = true
|
||||||
|
for p.running {
|
||||||
|
if err := p.ctx.Dispatch(); err != nil {
|
||||||
|
p.err = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
p.checkDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.err != nil {
|
||||||
|
return nil, p.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.pickedColor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) checkDone() {
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
picked, cancelled := ls.state.IsDone()
|
||||||
|
switch {
|
||||||
|
case cancelled:
|
||||||
|
p.running = false
|
||||||
|
return
|
||||||
|
case picked:
|
||||||
|
color, ok := ls.state.PickColor()
|
||||||
|
if ok {
|
||||||
|
p.pickedColor = &color
|
||||||
|
}
|
||||||
|
p.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) connect() error {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.display = display
|
||||||
|
p.ctx = display.Context()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) roundtrip() error {
|
||||||
|
return wlhelpers.Roundtrip(p.display, p.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupRegistry() error {
|
||||||
|
registry, err := p.display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.registry = registry
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
p.handleGlobal(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
delete(p.outputs, e.Name)
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case client.CompositorInterfaceName:
|
||||||
|
compositor := client.NewCompositor(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
|
||||||
|
p.compositor = compositor
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.ShmInterfaceName:
|
||||||
|
shm := client.NewShm(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||||
|
p.shm = shm
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.SeatInterfaceName:
|
||||||
|
seat := client.NewSeat(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
|
||||||
|
p.seat = seat
|
||||||
|
p.setupInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
output := client.NewOutput(p.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
p.outputs[e.Name] = &Output{
|
||||||
|
wlOutput: output,
|
||||||
|
globalName: e.Name,
|
||||||
|
scale: 1,
|
||||||
|
fractionalScale: 1.0,
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
p.setupOutputHandlers(e.Name, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||||
|
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
||||||
|
p.layerShell = layerShell
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
|
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 3 {
|
||||||
|
version = 3
|
||||||
|
}
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
||||||
|
p.screencopy = screencopy
|
||||||
|
}
|
||||||
|
|
||||||
|
case wp_viewporter.WpViewporterInterfaceName:
|
||||||
|
viewporter := wp_viewporter.NewWpViewporter(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, viewporter); err == nil {
|
||||||
|
p.viewporter = viewporter
|
||||||
|
}
|
||||||
|
|
||||||
|
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
||||||
|
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
p.shortcutsInhibitMgr = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
|
||||||
|
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.x = e.X
|
||||||
|
o.y = e.Y
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||||
|
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.width = e.Width
|
||||||
|
o.height = e.Height
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.scale = e.Factor
|
||||||
|
o.fractionalScale = float64(e.Factor)
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
if o, ok := p.outputs[name]; ok {
|
||||||
|
o.name = e.Name
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) createSurfaces() error {
|
||||||
|
p.outputsMu.Lock()
|
||||||
|
outputs := make([]*Output, 0, len(p.outputs))
|
||||||
|
for _, o := range p.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
}
|
||||||
|
p.outputsMu.Unlock()
|
||||||
|
|
||||||
|
for _, output := range outputs {
|
||||||
|
ls, err := p.createLayerSurface(output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("output %s: %w", output.name, err)
|
||||||
|
}
|
||||||
|
p.surfaces = append(p.surfaces, ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
|
||||||
|
surface, err := p.compositor.CreateSurface()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create surface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerSurf, err := p.layerShell.GetLayerSurface(
|
||||||
|
surface,
|
||||||
|
output.wlOutput,
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerShellV1LayerOverlay),
|
||||||
|
"dms-colorpicker",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get layer surface: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := &LayerSurface{
|
||||||
|
output: output,
|
||||||
|
state: NewSurfaceState(p.config.Format, p.config.Lowercase),
|
||||||
|
wlSurface: surface,
|
||||||
|
layerSurf: layerSurf,
|
||||||
|
hidden: true, // Start hidden, will show overlay when pointer enters
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.viewporter != nil {
|
||||||
|
vp, err := p.viewporter.GetViewport(surface)
|
||||||
|
if err == nil {
|
||||||
|
ls.viewport = vp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := layerSurf.SetAnchor(
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorTop) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorBottom) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorLeft) |
|
||||||
|
uint32(wlr_layer_shell.ZwlrLayerSurfaceV1AnchorRight),
|
||||||
|
); err != nil {
|
||||||
|
log.Warn("failed to set layer anchor", "err", err)
|
||||||
|
}
|
||||||
|
if err := layerSurf.SetExclusiveZone(-1); err != nil {
|
||||||
|
log.Warn("failed to set exclusive zone", "err", err)
|
||||||
|
}
|
||||||
|
if err := layerSurf.SetKeyboardInteractivity(uint32(wlr_layer_shell.ZwlrLayerSurfaceV1KeyboardInteractivityExclusive)); err != nil {
|
||||||
|
log.Warn("failed to set keyboard interactivity", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerSurf.SetConfigureHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ConfigureEvent) {
|
||||||
|
if err := layerSurf.AckConfigure(e.Serial); err != nil {
|
||||||
|
log.Warn("failed to ack configure", "err", err)
|
||||||
|
}
|
||||||
|
if err := ls.state.OnLayerConfigure(int(e.Width), int(e.Height)); err != nil {
|
||||||
|
log.Warn("failed to handle layer configure", "err", err)
|
||||||
|
}
|
||||||
|
ls.configured = true
|
||||||
|
|
||||||
|
scale := p.computeSurfaceScale(ls)
|
||||||
|
ls.state.SetScale(scale)
|
||||||
|
|
||||||
|
if !ls.state.IsReady() {
|
||||||
|
p.captureForSurface(ls)
|
||||||
|
} else {
|
||||||
|
p.redrawSurface(ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request shortcut inhibition once surface is configured
|
||||||
|
p.ensureShortcutsInhibitor(ls)
|
||||||
|
})
|
||||||
|
|
||||||
|
layerSurf.SetClosedHandler(func(e wlr_layer_shell.ZwlrLayerSurfaceV1ClosedEvent) {
|
||||||
|
p.running = false
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := surface.Commit(); err != nil {
|
||||||
|
log.Warn("failed to commit surface", "err", err)
|
||||||
|
}
|
||||||
|
return ls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
|
||||||
|
out := ls.output
|
||||||
|
if out == nil || out.scale <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return out.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
|
||||||
|
if p.shortcutsInhibitMgr == nil || p.seat == nil || p.shortcutsInhibitor != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inhibitor, err := p.shortcutsInhibitMgr.InhibitShortcuts(ls.wlSurface, p.seat)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("failed to create shortcuts inhibitor", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.shortcutsInhibitor = inhibitor
|
||||||
|
|
||||||
|
inhibitor.SetActiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1ActiveEvent) {
|
||||||
|
log.Debug("shortcuts inhibitor active")
|
||||||
|
})
|
||||||
|
|
||||||
|
inhibitor.SetInactiveHandler(func(e keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1InactiveEvent) {
|
||||||
|
log.Debug("shortcuts inhibitor deactivated by compositor")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) captureForSurface(ls *LayerSurface) {
|
||||||
|
frame, err := p.screencopy.CaptureOutput(0, ls.output.wlOutput)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.SetBufferHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferEvent) {
|
||||||
|
if err := ls.state.OnScreencopyBuffer(PixelFormat(e.Format), int(e.Width), int(e.Height), int(e.Stride)); err != nil {
|
||||||
|
log.Error("failed to create screencopy buffer", "err", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetBufferDoneHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1BufferDoneEvent) {
|
||||||
|
screenBuf := ls.state.ScreenBuffer()
|
||||||
|
if screenBuf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := p.shm.CreatePool(screenBuf.Fd(), int32(screenBuf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wlBuffer, err := pool.CreateBuffer(0, int32(screenBuf.Width), int32(screenBuf.Height), int32(screenBuf.Stride), uint32(ls.state.screenFormat))
|
||||||
|
if err != nil {
|
||||||
|
pool.Destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.scopyBuffer != nil {
|
||||||
|
ls.scopyBuffer.Destroy()
|
||||||
|
}
|
||||||
|
ls.scopyBuffer = wlBuffer
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
|
||||||
|
|
||||||
|
if err := frame.Copy(wlBuffer); err != nil {
|
||||||
|
log.Error("failed to copy frame", "err", err)
|
||||||
|
}
|
||||||
|
pool.Destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFlagsHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FlagsEvent) {
|
||||||
|
ls.state.OnScreencopyFlags(e.Flags)
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||||
|
ls.state.OnScreencopyReady()
|
||||||
|
|
||||||
|
logicalW, _ := ls.state.LogicalSize()
|
||||||
|
screenBuf := ls.state.ScreenBuffer()
|
||||||
|
if logicalW > 0 && screenBuf != nil {
|
||||||
|
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := p.computeSurfaceScale(ls)
|
||||||
|
ls.state.SetScale(scale)
|
||||||
|
frame.Destroy()
|
||||||
|
p.redrawSurface(ls)
|
||||||
|
})
|
||||||
|
|
||||||
|
frame.SetFailedHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1FailedEvent) {
|
||||||
|
frame.Destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||||
|
var renderBuf *ShmBuffer
|
||||||
|
if ls.hidden {
|
||||||
|
renderBuf = ls.state.RedrawScreenOnly()
|
||||||
|
} else {
|
||||||
|
renderBuf = ls.state.Redraw()
|
||||||
|
}
|
||||||
|
if renderBuf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.oldBuffer != nil {
|
||||||
|
ls.oldBuffer.Destroy()
|
||||||
|
ls.oldBuffer = nil
|
||||||
|
}
|
||||||
|
if ls.oldPool != nil {
|
||||||
|
ls.oldPool.Destroy()
|
||||||
|
ls.oldPool = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ls.oldPool = ls.wlPool
|
||||||
|
ls.oldBuffer = ls.wlBuffer
|
||||||
|
ls.wlPool = nil
|
||||||
|
ls.wlBuffer = nil
|
||||||
|
|
||||||
|
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.wlPool = pool
|
||||||
|
|
||||||
|
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.wlBuffer = wlBuffer
|
||||||
|
|
||||||
|
lsRef := ls
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
lsRef.bufferBusy = false
|
||||||
|
})
|
||||||
|
ls.bufferBusy = true
|
||||||
|
|
||||||
|
logicalW, logicalH := ls.state.LogicalSize()
|
||||||
|
if logicalW == 0 || logicalH == 0 {
|
||||||
|
logicalW = int(ls.output.width)
|
||||||
|
logicalH = int(ls.output.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ls.viewport != nil {
|
||||||
|
_ = ls.wlSurface.SetBufferScale(1)
|
||||||
|
_ = ls.viewport.SetSource(0, 0, float64(renderBuf.Width), float64(renderBuf.Height))
|
||||||
|
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
|
||||||
|
} else {
|
||||||
|
bufferScale := ls.output.scale
|
||||||
|
if bufferScale <= 0 {
|
||||||
|
bufferScale = 1
|
||||||
|
}
|
||||||
|
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||||
|
}
|
||||||
|
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||||
|
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||||
|
_ = ls.wlSurface.Commit()
|
||||||
|
|
||||||
|
ls.state.SwapBuffers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) hideSurface(ls *LayerSurface) {
|
||||||
|
if ls == nil || ls.wlSurface == nil || ls.hidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.hidden = true
|
||||||
|
// Redraw without the crosshair overlay
|
||||||
|
p.redrawSurface(ls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupInput() {
|
||||||
|
if p.seat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
|
||||||
|
pointer, err := p.seat.GetPointer()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.pointer = pointer
|
||||||
|
p.setupPointerHandlers()
|
||||||
|
}
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
|
||||||
|
keyboard, err := p.seat.GetKeyboard()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.keyboard = keyboard
|
||||||
|
p.setupKeyboardHandlers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupPointerHandlers() {
|
||||||
|
p.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
|
||||||
|
if err := p.pointer.SetCursor(e.Serial, nil, 0, 0); err != nil {
|
||||||
|
log.Debug("failed to hide cursor", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.activeSurface = nil
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
|
p.activeSurface = ls
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.activeSurface.hidden {
|
||||||
|
p.activeSurface.hidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
|
p.redrawSurface(p.activeSurface)
|
||||||
|
})
|
||||||
|
|
||||||
|
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
|
p.hideSurface(ls)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
p.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
|
||||||
|
if p.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
|
p.redrawSurface(p.activeSurface)
|
||||||
|
})
|
||||||
|
|
||||||
|
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||||
|
if p.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.activeSurface.state.OnPointerButton(e.Button, e.State)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) setupKeyboardHandlers() {
|
||||||
|
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
ls.state.OnKey(e.Key, e.State)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Picker) cleanup() {
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
if ls.scopyBuffer != nil {
|
||||||
|
ls.scopyBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.oldBuffer != nil {
|
||||||
|
ls.oldBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.oldPool != nil {
|
||||||
|
ls.oldPool.Destroy()
|
||||||
|
}
|
||||||
|
if ls.wlBuffer != nil {
|
||||||
|
ls.wlBuffer.Destroy()
|
||||||
|
}
|
||||||
|
if ls.wlPool != nil {
|
||||||
|
ls.wlPool.Destroy()
|
||||||
|
}
|
||||||
|
if ls.viewport != nil {
|
||||||
|
ls.viewport.Destroy()
|
||||||
|
}
|
||||||
|
if ls.layerSurf != nil {
|
||||||
|
ls.layerSurf.Destroy()
|
||||||
|
}
|
||||||
|
if ls.wlSurface != nil {
|
||||||
|
ls.wlSurface.Destroy()
|
||||||
|
}
|
||||||
|
if ls.state != nil {
|
||||||
|
ls.state.Destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.shortcutsInhibitor != nil {
|
||||||
|
if err := p.shortcutsInhibitor.Destroy(); err != nil {
|
||||||
|
log.Debug("failed to destroy shortcuts inhibitor", "err", err)
|
||||||
|
}
|
||||||
|
p.shortcutsInhibitor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.shortcutsInhibitMgr != nil {
|
||||||
|
if err := p.shortcutsInhibitMgr.Destroy(); err != nil {
|
||||||
|
log.Debug("failed to destroy shortcuts inhibit manager", "err", err)
|
||||||
|
}
|
||||||
|
p.shortcutsInhibitMgr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.viewporter != nil {
|
||||||
|
p.viewporter.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.screencopy != nil {
|
||||||
|
p.screencopy.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.pointer != nil {
|
||||||
|
p.pointer.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.keyboard != nil {
|
||||||
|
p.keyboard.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.display != nil {
|
||||||
|
p.ctx.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
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],
|
||||||
|
}
|
||||||
|
}
|
||||||
1132
core/internal/colorpicker/state.go
Normal file
1132
core/internal/colorpicker/state.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -110,7 +110,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deployNiriConfig handles Niri configuration deployment with backup and merging
|
|
||||||
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Niri",
|
ConfigType: "Niri",
|
||||||
@@ -123,6 +122,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
var existingConfig string
|
var existingConfig string
|
||||||
if _, err := os.Stat(result.Path); err == nil {
|
if _, err := os.Stat(result.Path); err == nil {
|
||||||
cd.log("Found existing Niri configuration")
|
cd.log("Found existing Niri configuration")
|
||||||
@@ -143,14 +148,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect polkit agent path
|
|
||||||
polkitPath, err := cd.detectPolkitAgent()
|
polkitPath, err := cd.detectPolkitAgent()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine terminal command based on choice
|
|
||||||
var terminalCommand string
|
var terminalCommand string
|
||||||
switch terminal {
|
switch terminal {
|
||||||
case deps.TerminalGhostty:
|
case deps.TerminalGhostty:
|
||||||
@@ -160,13 +163,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
case deps.TerminalAlacritty:
|
case deps.TerminalAlacritty:
|
||||||
terminalCommand = "alacritty"
|
terminalCommand = "alacritty"
|
||||||
default:
|
default:
|
||||||
terminalCommand = "ghostty" // fallback to ghostty
|
terminalCommand = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
|
||||||
// If there was an existing config, merge the output sections
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,11 +184,38 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := cd.deployNiriDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
result.Deployed = true
|
result.Deployed = true
|
||||||
cd.log("Successfully deployed Niri configuration")
|
cd.log("Successfully deployed Niri configuration")
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) error {
|
||||||
|
configs := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
}{
|
||||||
|
{"colors.kdl", NiriColorsConfig},
|
||||||
|
{"layout.kdl", NiriLayoutConfig},
|
||||||
|
{"alttab.kdl", NiriAlttabConfig},
|
||||||
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
|
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
content, err := os.ReadFile(result.Path)
|
content, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||||
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
assert.Contains(t, string(content), "bind = $mod, T, exec, $TERMINAL")
|
||||||
assert.Contains(t, string(content), "exec-once = ")
|
assert.Contains(t, string(content), "exec-once = ")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ general {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||||
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
assert.Contains(t, string(newContent), "bind = $mod, T, exec, $TERMINAL")
|
||||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -479,23 +479,22 @@ general {
|
|||||||
func TestNiriConfigStructure(t *testing.T) {
|
func TestNiriConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, NiriConfig, "input {")
|
assert.Contains(t, NiriConfig, "input {")
|
||||||
assert.Contains(t, NiriConfig, "layout {")
|
assert.Contains(t, NiriConfig, "layout {")
|
||||||
assert.Contains(t, NiriConfig, "binds {")
|
|
||||||
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
||||||
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
|
||||||
|
assert.Contains(t, NiriBindsConfig, "binds {")
|
||||||
|
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandConfigStructure(t *testing.T) {
|
func TestHyprlandConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
|
|
||||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||||
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
|
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
|
||||||
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}")
|
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, $TERMINAL")
|
||||||
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||||
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
assert.Contains(t, HyprlandConfig, "windowrule {")
|
||||||
|
assert.Contains(t, HyprlandConfig, "match:class = ^(com\\.mitchellh\\.ghostty)$")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
|
|||||||
@@ -7,20 +7,10 @@
|
|||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||||
monitor = , preferred,auto,auto
|
monitor = , preferred,auto,auto
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ENVIRONMENT VARS
|
|
||||||
# ==================
|
|
||||||
env = QT_QPA_PLATFORM,wayland
|
|
||||||
env = ELECTRON_OZONE_PLATFORM_HINT,auto
|
|
||||||
env = QT_QPA_PLATFORMTHEME,gtk3
|
|
||||||
env = QT_QPA_PLATFORMTHEME_QT6,gtk3
|
|
||||||
env = TERMINAL,{{TERMINAL_COMMAND}}
|
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# STARTUP APPS
|
# STARTUP APPS
|
||||||
# ==================
|
# ==================
|
||||||
exec-once = bash -c "wl-paste --watch cliphist store &"
|
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||||
exec-once = dms run
|
|
||||||
exec-once = {{POLKIT_AGENT_PATH}}
|
exec-once = {{POLKIT_AGENT_PATH}}
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
@@ -100,36 +90,132 @@ misc {
|
|||||||
# ==================
|
# ==================
|
||||||
# WINDOW RULES
|
# WINDOW RULES
|
||||||
# ==================
|
# ==================
|
||||||
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
|
windowrule {
|
||||||
|
name = windowrule-1
|
||||||
|
tile = on
|
||||||
|
match:class = ^(org\.wezfurlong\.wezterm)$
|
||||||
|
border_size = 0
|
||||||
|
}
|
||||||
|
|
||||||
windowrulev2 = rounding 12, class:^(org\.gnome\.)
|
|
||||||
windowrulev2 = noborder, class:^(org\.gnome\.)
|
|
||||||
|
|
||||||
windowrulev2 = tile, class:^(gnome-control-center)$
|
windowrule {
|
||||||
windowrulev2 = tile, class:^(pavucontrol)$
|
name = windowrule-2
|
||||||
windowrulev2 = tile, class:^(nm-connection-editor)$
|
rounding = 12
|
||||||
|
match:class = ^(org\.gnome\.)
|
||||||
|
border_size = 0
|
||||||
|
}
|
||||||
|
|
||||||
windowrulev2 = float, class:^(gnome-calculator)$
|
|
||||||
windowrulev2 = float, class:^(galculator)$
|
|
||||||
windowrulev2 = float, class:^(blueman-manager)$
|
|
||||||
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
|
|
||||||
windowrulev2 = float, class:^(steam)$
|
|
||||||
windowrulev2 = float, class:^(xdg-desktop-portal)$
|
|
||||||
|
|
||||||
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
|
|
||||||
windowrulev2 = noborder, class:^(Alacritty)$
|
|
||||||
windowrulev2 = noborder, class:^(zen)$
|
|
||||||
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
|
|
||||||
windowrulev2 = noborder, class:^(kitty)$
|
|
||||||
|
|
||||||
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
windowrule {
|
||||||
windowrulev2 = float, class:^(zoom)$
|
name = windowrule-3
|
||||||
|
tile = on
|
||||||
|
match:class = ^(gnome-control-center)$
|
||||||
|
}
|
||||||
|
|
||||||
# DMS windows floating by default
|
windowrule {
|
||||||
windowrulev2 = float, class:^(org.quickshell)$
|
name = windowrule-4
|
||||||
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
tile = on
|
||||||
|
match:class = ^(pavucontrol)$
|
||||||
|
}
|
||||||
|
|
||||||
layerrule = noanim, ^(quickshell)$
|
windowrule {
|
||||||
|
name = windowrule-5
|
||||||
|
tile = on
|
||||||
|
match:class = ^(nm-connection-editor)$
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-6
|
||||||
|
float = on
|
||||||
|
match:class = ^(gnome-calculator)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-7
|
||||||
|
float = on
|
||||||
|
match:class = ^(galculator)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-8
|
||||||
|
float = on
|
||||||
|
match:class = ^(blueman-manager)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-9
|
||||||
|
float = on
|
||||||
|
match:class = ^(org\.gnome\.Nautilus)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-10
|
||||||
|
float = on
|
||||||
|
match:class = ^(steam)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-11
|
||||||
|
float = on
|
||||||
|
match:class = ^(xdg-desktop-portal)$
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-12
|
||||||
|
border_size = 0
|
||||||
|
match:class = ^(Alacritty)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-13
|
||||||
|
border_size = 0
|
||||||
|
match:class = ^(zen)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-14
|
||||||
|
border_size = 0
|
||||||
|
match:class = ^(com\.mitchellh\.ghostty)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-15
|
||||||
|
border_size = 0
|
||||||
|
match:class = ^(kitty)$
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-16
|
||||||
|
float = on
|
||||||
|
match:class = ^(firefox)$
|
||||||
|
match:title = ^(Picture-in-Picture)$
|
||||||
|
}
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-17
|
||||||
|
float = on
|
||||||
|
match:class = ^(zoom)$
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
windowrule {
|
||||||
|
name = windowrule-18
|
||||||
|
opacity = 0.9 0.9
|
||||||
|
match:float = 0
|
||||||
|
match:focus = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
layerrule {
|
||||||
|
name = layerrule-1
|
||||||
|
no_anim = on
|
||||||
|
match:namespace = ^(quickshell)$
|
||||||
|
}
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# KEYBINDINGS
|
# KEYBINDINGS
|
||||||
@@ -137,7 +223,7 @@ layerrule = noanim, ^(quickshell)$
|
|||||||
$mod = SUPER
|
$mod = SUPER
|
||||||
|
|
||||||
# === Application Launchers ===
|
# === Application Launchers ===
|
||||||
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
bind = $mod, T, exec, $TERMINAL
|
||||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
bind = $mod, space, exec, dms ipc call spotlight toggle
|
||||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
bind = $mod, V, exec, dms ipc call clipboard toggle
|
||||||
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
|
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
|
||||||
@@ -281,12 +367,9 @@ binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
|||||||
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
||||||
|
|
||||||
# === Screenshots ===
|
# === Screenshots ===
|
||||||
bind = , XF86Launch1, exec, grimblast copy area
|
bind = , Print, exec, dms screenshot
|
||||||
bind = CTRL, XF86Launch1, exec, grimblast copy screen
|
bind = CTRL, Print, exec, dms screenshot full
|
||||||
bind = ALT, XF86Launch1, exec, grimblast copy active
|
bind = ALT, Print, exec, dms screenshot window
|
||||||
bind = , Print, exec, grimblast copy area
|
|
||||||
bind = CTRL, Print, exec, grimblast copy screen
|
|
||||||
bind = ALT, Print, exec, grimblast copy active
|
|
||||||
|
|
||||||
# === System Controls ===
|
# === System Controls ===
|
||||||
bind = $mod SHIFT, P, dpms, off
|
bind = $mod SHIFT, P, dpms, off
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -116,15 +116,9 @@ overview {
|
|||||||
// See the binds section below for more spawn examples.
|
// See the binds section below for more spawn examples.
|
||||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
||||||
spawn-at-startup "dms" "run"
|
|
||||||
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
||||||
environment {
|
environment {
|
||||||
XDG_CURRENT_DESKTOP "niri"
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
QT_QPA_PLATFORM "wayland"
|
|
||||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
|
||||||
QT_QPA_PLATFORMTHEME "gtk3"
|
|
||||||
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
|
||||||
TERMINAL "{{TERMINAL_COMMAND}}"
|
|
||||||
}
|
}
|
||||||
hotkey-overlay {
|
hotkey-overlay {
|
||||||
skip-at-startup
|
skip-at-startup
|
||||||
@@ -214,210 +208,27 @@ window-rule {
|
|||||||
match app-id="zoom"
|
match app-id="zoom"
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
window-rule {
|
|
||||||
geometry-corner-radius 12
|
|
||||||
clip-to-geometry true
|
|
||||||
}
|
|
||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
binds {
|
|
||||||
// === System & Overview ===
|
|
||||||
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
|
|
||||||
Mod+Tab repeat=false { toggle-overview; }
|
|
||||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
|
||||||
|
|
||||||
// === Application Launchers ===
|
|
||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
|
|
||||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
|
||||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
|
||||||
}
|
|
||||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
|
||||||
}
|
|
||||||
Mod+M hotkey-overlay-title="Task Manager" {
|
|
||||||
spawn "dms" "ipc" "call" "processlist" "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 {
|
debug {
|
||||||
honor-xdg-activation-with-invalid-serial
|
honor-xdg-activation-with-invalid-serial
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override to disable super+tab
|
||||||
|
recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
Alt+Shift+Tab { previous-window scope="output"; }
|
||||||
|
Alt+grave { next-window filter="app-id"; }
|
||||||
|
Alt+Shift+grave { previous-window filter="app-id"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include dms files
|
||||||
|
include "dms/colors.kdl"
|
||||||
|
include "dms/layout.kdl"
|
||||||
|
include "dms/alttab.kdl"
|
||||||
|
include "dms/binds.kdl"
|
||||||
|
|||||||
@@ -4,3 +4,15 @@ import _ "embed"
|
|||||||
|
|
||||||
//go:embed embedded/niri.kdl
|
//go:embed embedded/niri.kdl
|
||||||
var NiriConfig string
|
var NiriConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-colors.kdl
|
||||||
|
var NiriColorsConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-layout.kdl
|
||||||
|
var NiriLayoutConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-alttab.kdl
|
||||||
|
var NiriAlttabConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-binds.kdl
|
||||||
|
var NiriBindsConfig string
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ type ColorInfo struct {
|
|||||||
B int `json:"b"`
|
B int `json:"b"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VariantColorValue struct {
|
||||||
|
Hex string `json:"hex"`
|
||||||
|
HexStripped string `json:"hex_stripped"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VariantColorInfo struct {
|
||||||
|
Dark VariantColorValue `json:"dark"`
|
||||||
|
Light VariantColorValue `json:"light"`
|
||||||
|
Default VariantColorValue `json:"default"`
|
||||||
|
}
|
||||||
|
|
||||||
type Palette struct {
|
type Palette struct {
|
||||||
Color0 ColorInfo `json:"color0"`
|
Color0 ColorInfo `json:"color0"`
|
||||||
Color1 ColorInfo `json:"color1"`
|
Color1 ColorInfo `json:"color1"`
|
||||||
@@ -42,6 +53,25 @@ type Palette struct {
|
|||||||
Color15 ColorInfo `json:"color15"`
|
Color15 ColorInfo `json:"color15"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VariantPalette struct {
|
||||||
|
Color0 VariantColorInfo `json:"color0"`
|
||||||
|
Color1 VariantColorInfo `json:"color1"`
|
||||||
|
Color2 VariantColorInfo `json:"color2"`
|
||||||
|
Color3 VariantColorInfo `json:"color3"`
|
||||||
|
Color4 VariantColorInfo `json:"color4"`
|
||||||
|
Color5 VariantColorInfo `json:"color5"`
|
||||||
|
Color6 VariantColorInfo `json:"color6"`
|
||||||
|
Color7 VariantColorInfo `json:"color7"`
|
||||||
|
Color8 VariantColorInfo `json:"color8"`
|
||||||
|
Color9 VariantColorInfo `json:"color9"`
|
||||||
|
Color10 VariantColorInfo `json:"color10"`
|
||||||
|
Color11 VariantColorInfo `json:"color11"`
|
||||||
|
Color12 VariantColorInfo `json:"color12"`
|
||||||
|
Color13 VariantColorInfo `json:"color13"`
|
||||||
|
Color14 VariantColorInfo `json:"color14"`
|
||||||
|
Color15 VariantColorInfo `json:"color15"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewColorInfo(hex string) ColorInfo {
|
func NewColorInfo(hex string) ColorInfo {
|
||||||
rgb := HexToRGB(hex)
|
rgb := HexToRGB(hex)
|
||||||
stripped := hex
|
stripped := hex
|
||||||
@@ -492,3 +522,54 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
|
|
||||||
return palette
|
return palette
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VariantOptions struct {
|
||||||
|
PrimaryDark string
|
||||||
|
PrimaryLight string
|
||||||
|
Background string
|
||||||
|
UseDPS bool
|
||||||
|
IsLightMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
|
||||||
|
darkVal := VariantColorValue{Hex: dark.Hex, HexStripped: dark.HexStripped}
|
||||||
|
lightVal := VariantColorValue{Hex: light.Hex, HexStripped: light.HexStripped}
|
||||||
|
|
||||||
|
defaultVal := darkVal
|
||||||
|
if isLightMode {
|
||||||
|
defaultVal = lightVal
|
||||||
|
}
|
||||||
|
|
||||||
|
return VariantColorInfo{
|
||||||
|
Dark: darkVal,
|
||||||
|
Light: lightVal,
|
||||||
|
Default: defaultVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateVariantPalette(opts VariantOptions) VariantPalette {
|
||||||
|
darkOpts := PaletteOptions{IsLight: false, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||||
|
lightOpts := PaletteOptions{IsLight: true, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||||
|
|
||||||
|
dark := GeneratePalette(opts.PrimaryDark, darkOpts)
|
||||||
|
light := GeneratePalette(opts.PrimaryLight, lightOpts)
|
||||||
|
|
||||||
|
return VariantPalette{
|
||||||
|
Color0: mergeColorInfo(dark.Color0, light.Color0, opts.IsLightMode),
|
||||||
|
Color1: mergeColorInfo(dark.Color1, light.Color1, opts.IsLightMode),
|
||||||
|
Color2: mergeColorInfo(dark.Color2, light.Color2, opts.IsLightMode),
|
||||||
|
Color3: mergeColorInfo(dark.Color3, light.Color3, opts.IsLightMode),
|
||||||
|
Color4: mergeColorInfo(dark.Color4, light.Color4, opts.IsLightMode),
|
||||||
|
Color5: mergeColorInfo(dark.Color5, light.Color5, opts.IsLightMode),
|
||||||
|
Color6: mergeColorInfo(dark.Color6, light.Color6, opts.IsLightMode),
|
||||||
|
Color7: mergeColorInfo(dark.Color7, light.Color7, opts.IsLightMode),
|
||||||
|
Color8: mergeColorInfo(dark.Color8, light.Color8, opts.IsLightMode),
|
||||||
|
Color9: mergeColorInfo(dark.Color9, light.Color9, opts.IsLightMode),
|
||||||
|
Color10: mergeColorInfo(dark.Color10, light.Color10, opts.IsLightMode),
|
||||||
|
Color11: mergeColorInfo(dark.Color11, light.Color11, opts.IsLightMode),
|
||||||
|
Color12: mergeColorInfo(dark.Color12, light.Color12, opts.IsLightMode),
|
||||||
|
Color13: mergeColorInfo(dark.Color13, light.Color13, opts.IsLightMode),
|
||||||
|
Color14: mergeColorInfo(dark.Color14, light.Color14, opts.IsLightMode),
|
||||||
|
Color15: mergeColorInfo(dark.Color15, light.Color15, opts.IsLightMode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ func GenerateJSON(p Palette) string {
|
|||||||
return string(marshalled)
|
return string(marshalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateVariantJSON(p VariantPalette) string {
|
||||||
|
marshalled, _ := json.Marshal(p)
|
||||||
|
return string(marshalled)
|
||||||
|
}
|
||||||
|
|
||||||
func GenerateKittyTheme(p Palette) string {
|
func GenerateKittyTheme(p Palette) string {
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
|
fmt.Fprintf(&result, "color0 %s\n", p.Color0.Hex)
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, a.detectQuickshell())
|
dependencies = append(dependencies, a.detectQuickshell())
|
||||||
dependencies = append(dependencies, a.detectXDGPortal())
|
dependencies = append(dependencies, a.detectXDGPortal())
|
||||||
dependencies = append(dependencies, a.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, a.detectAccountsService())
|
dependencies = append(dependencies, a.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -107,7 +106,6 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
dependencies = append(dependencies, a.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, a.detectClipboardTools()...)
|
dependencies = append(dependencies, a.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -127,20 +125,6 @@ func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if a.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if a.packageInstalled("accountsservice") {
|
if a.packageInstalled("accountsservice") {
|
||||||
@@ -178,18 +162,13 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = a.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = a.getNiriMapping(variants["niri"])
|
packages["niri"] = a.getNiriMapping(variants["niri"])
|
||||||
@@ -378,6 +357,15 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := a.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := a.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.EnableDMSService(ctx); err != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 7: Complete
|
// Phase 7: Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
const forceQuickshellGit = false
|
const (
|
||||||
const forceDMSGit = false
|
forceQuickshellGit = false
|
||||||
|
forceDMSGit = false
|
||||||
|
)
|
||||||
|
|
||||||
// BaseDistribution provides common functionality for all distributions
|
// BaseDistribution provides common functionality for all distributions
|
||||||
type BaseDistribution struct {
|
type BaseDistribution struct {
|
||||||
@@ -219,20 +221,6 @@ func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
|||||||
return dependencies
|
return dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if b.commandExists("hyprpicker") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "hyprpicker",
|
|
||||||
Status: status,
|
|
||||||
Description: "Color picker for Wayland",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||||
var dependencies []deps.Dependency
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
@@ -240,10 +228,7 @@ func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
|||||||
name string
|
name string
|
||||||
description string
|
description string
|
||||||
}{
|
}{
|
||||||
{"grim", "Screenshot utility for Wayland"},
|
|
||||||
{"slurp", "Region selection utility for Wayland"},
|
|
||||||
{"hyprctl", "Hyprland control utility"},
|
{"hyprctl", "Hyprland control utility"},
|
||||||
{"grimblast", "Screenshot script for Hyprland"},
|
|
||||||
{"jq", "JSON processor"},
|
{"jq", "JSON processor"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,6 +549,68 @@ func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressCha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) DetectTerminalFromDeps(dependencies []deps.Dependency) deps.Terminal {
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
switch dep.Name {
|
||||||
|
case "ghostty":
|
||||||
|
return deps.TerminalGhostty
|
||||||
|
case "kitty":
|
||||||
|
return deps.TerminalKitty
|
||||||
|
case "alacritty":
|
||||||
|
return deps.TerminalAlacritty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps.TerminalGhostty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
||||||
|
if err := os.MkdirAll(envDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var terminalCmd string
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
terminalCmd = "ghostty"
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
terminalCmd = "kitty"
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
terminalCmd = "alacritty"
|
||||||
|
default:
|
||||||
|
terminalCmd = "ghostty"
|
||||||
|
}
|
||||||
|
|
||||||
|
content := fmt.Sprintf(`QT_QPA_PLATFORM=wayland
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||||
|
QT_QPA_PLATFORMTHEME=gtk3
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6=gtk3
|
||||||
|
TERMINAL=%s
|
||||||
|
`, terminalCmd)
|
||||||
|
|
||||||
|
envFile := filepath.Join(envDir, "90-dms.conf")
|
||||||
|
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write environment config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.log(fmt.Sprintf("Wrote environment config to %s", envFile))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) EnableDMSService(ctx context.Context) error {
|
||||||
|
cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable dms service: %w", err)
|
||||||
|
}
|
||||||
|
b.log("Enabled dms systemd user service")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// installDMSBinary installs the DMS binary from GitHub releases
|
// installDMSBinary installs the DMS binary from GitHub releases
|
||||||
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
b.log("Installing/updating DMS binary...")
|
b.log("Installing/updating DMS binary...")
|
||||||
@@ -602,7 +649,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
|||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
}
|
}
|
||||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, d.detectQuickshell())
|
dependencies = append(dependencies, d.detectQuickshell())
|
||||||
dependencies = append(dependencies, d.detectXDGPortal())
|
dependencies = append(dependencies, d.detectXDGPortal())
|
||||||
dependencies = append(dependencies, d.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, d.detectAccountsService())
|
dependencies = append(dependencies, d.detectAccountsService())
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
@@ -70,7 +69,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, d.detectMatugen())
|
dependencies = append(dependencies, d.detectMatugen())
|
||||||
dependencies = append(dependencies, d.detectDgop())
|
dependencies = append(dependencies, d.detectDgop())
|
||||||
dependencies = append(dependencies, d.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, d.detectClipboardTools()...)
|
dependencies = append(dependencies, d.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -90,20 +88,6 @@ func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if d.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if d.commandExists("xwayland-satellite") {
|
if d.commandExists("xwayland-satellite") {
|
||||||
@@ -139,33 +123,65 @@ func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
packages := map[string]PackageMapping{
|
packages := map[string]PackageMapping{
|
||||||
|
// Standard APT packages
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
// DMS packages from OBS with variant support
|
||||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
niriVariant := variants["niri"]
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
packages["niri"] = d.getNiriMapping(niriVariant)
|
||||||
|
packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhasePrerequisites,
|
Phase: PhasePrerequisites,
|
||||||
@@ -238,8 +254,23 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, obsPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
// Enable OBS repositories
|
||||||
|
if len(obsPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Enabling OBS repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Setting up OBS repositories for additional packages",
|
||||||
|
}
|
||||||
|
if err := d.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable OBS repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -254,6 +285,22 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OBS Packages
|
||||||
|
obsPkgNames := d.extractPackageNames(obsPkgs)
|
||||||
|
if len(obsPkgNames) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := d.installAPTPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Builds
|
||||||
if len(manualPkgs) > 0 {
|
if len(manualPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -286,6 +333,15 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := d.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := d.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.EnableDMSService(ctx); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
Progress: 1.0,
|
Progress: 1.0,
|
||||||
@@ -297,8 +353,9 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
|
||||||
systemPkgs := []string{}
|
systemPkgs := []string{}
|
||||||
|
obsPkgs := []PackageMapping{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
|
|
||||||
variantMap := make(map[string]deps.PackageVariant)
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
@@ -306,7 +363,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
variantMap[dep.Name] = dep.Variant
|
variantMap[dep.Name] = dep.Variant
|
||||||
}
|
}
|
||||||
|
|
||||||
packageMap := d.GetPackageMapping(wm)
|
packageMap := d.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
for _, dep := range dependencies {
|
for _, dep := range dependencies {
|
||||||
if disabledFlags[dep.Name] {
|
if disabledFlags[dep.Name] {
|
||||||
@@ -326,12 +383,116 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
switch pkgInfo.Repository {
|
switch pkgInfo.Repository {
|
||||||
case RepoTypeSystem:
|
case RepoTypeSystem:
|
||||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeOBS:
|
||||||
|
obsPkgs = append(obsPkgs, pkgInfo)
|
||||||
case RepoTypeManual:
|
case RepoTypeManual:
|
||||||
manualPkgs = append(manualPkgs, dep.Name)
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
osInfo, err := GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get OS info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine Debian version for OBS repository URL
|
||||||
|
debianVersion := "Debian_13"
|
||||||
|
if osInfo.VersionID == "testing" {
|
||||||
|
debianVersion = "Debian_Testing"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range obsPkgs {
|
||||||
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
|
d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||||
|
|
||||||
|
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||||
|
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||||
|
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||||
|
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion)
|
||||||
|
|
||||||
|
// Check if repository already exists
|
||||||
|
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
|
||||||
|
checkCmd := exec.CommandContext(ctx, "test", "-f", listFile)
|
||||||
|
if checkCmd.Run() == nil {
|
||||||
|
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||||
|
|
||||||
|
// Create keyrings directory if it doesn't exist
|
||||||
|
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
|
if err := mkdirCmd.Run(); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.18,
|
||||||
|
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath)
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
||||||
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add repository
|
||||||
|
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.20,
|
||||||
|
Step: fmt.Sprintf("Adding OBS repository %s...", pkg.RepoURL),
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
|
}
|
||||||
|
|
||||||
|
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile))
|
||||||
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
d.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(enabledRepos) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.25,
|
||||||
|
Step: "Updating package lists...",
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo apt-get update",
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||||
|
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
|
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -494,30 +655,6 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
d.log("Installing Ghostty using Debian installer script...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Running Ghostty Debian installer...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
|
||||||
LogOutput: "Installing Ghostty using pre-built Debian package",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
|
||||||
|
|
||||||
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.log("Ghostty installed successfully using Debian installer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -527,10 +664,6 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "ghostty":
|
|
||||||
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, f.detectQuickshell())
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
dependencies = append(dependencies, f.detectXDGPortal())
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
dependencies = append(dependencies, f.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, f.detectAccountsService())
|
dependencies = append(dependencies, f.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -92,7 +91,6 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
dependencies = append(dependencies, f.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, f.detectClipboardTools()...)
|
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -112,20 +110,6 @@ func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if f.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("rpm", "-q", pkg)
|
cmd := exec.Command("rpm", "-q", pkg)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
@@ -145,9 +129,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
|
|
||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
@@ -160,10 +142,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = f.getNiriMapping(variants["niri"])
|
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||||
@@ -194,13 +173,6 @@ func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) Pac
|
|||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
|
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
if variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||||
@@ -385,6 +357,15 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := f.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := f.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.EnableDMSService(ctx); err != nil {
|
||||||
|
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 7: Complete
|
// Phase 7: Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, g.detectMatugen())
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
dependencies = append(dependencies, g.detectDgop())
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
dependencies = append(dependencies, g.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, g.detectClipboardTools()...)
|
dependencies = append(dependencies, g.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -190,7 +189,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||||
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
|
|
||||||
|
|
||||||
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
||||||
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||||
@@ -207,10 +205,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
||||||
packages["grimblast"] = PackageMapping{Name: "gui-wm/hyprland-contrib", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
|
||||||
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = g.getNiriMapping(variants["niri"])
|
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||||
@@ -236,10 +231,6 @@ func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) Pac
|
|||||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
|
|
||||||
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
||||||
}
|
}
|
||||||
@@ -460,6 +451,15 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := g.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := g.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EnableDMSService(ctx); err != nil {
|
||||||
|
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
Progress: 1.0,
|
Progress: 1.0,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const (
|
|||||||
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
|
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
|
||||||
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
|
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
|
||||||
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
|
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
|
||||||
|
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
|
||||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install dgop: %w", err)
|
return fmt.Errorf("failed to install dgop: %w", err)
|
||||||
}
|
}
|
||||||
case "grimblast":
|
|
||||||
if err := m.installGrimblast(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
|
||||||
}
|
|
||||||
case "niri":
|
case "niri":
|
||||||
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install niri: %w", err)
|
return fmt.Errorf("failed to install niri: %w", err)
|
||||||
@@ -166,62 +162,6 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installGrimblast(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
m.log("Installing grimblast script for Hyprland...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Downloading grimblast script...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "curl grimblast script",
|
|
||||||
}
|
|
||||||
|
|
||||||
grimblastURL := "https://raw.githubusercontent.com/hyprwm/contrib/refs/heads/main/grimblast/grimblast"
|
|
||||||
tmpPath := filepath.Join(os.TempDir(), "grimblast")
|
|
||||||
|
|
||||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", "-o", tmpPath, grimblastURL)
|
|
||||||
if err := downloadCmd.Run(); err != nil {
|
|
||||||
m.logError("failed to download grimblast", err)
|
|
||||||
return fmt.Errorf("failed to download grimblast: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.5,
|
|
||||||
Step: "Making grimblast executable...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "chmod +x grimblast",
|
|
||||||
}
|
|
||||||
|
|
||||||
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", tmpPath)
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
m.logError("failed to make grimblast executable", err)
|
|
||||||
return fmt.Errorf("failed to make grimblast executable: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.8,
|
|
||||||
Step: "Installing grimblast to /usr/local/bin...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "sudo cp grimblast /usr/local/bin/",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
fmt.Sprintf("cp %s /usr/local/bin/grimblast", tmpPath))
|
|
||||||
if err := installCmd.Run(); err != nil {
|
|
||||||
m.logError("failed to install grimblast", err)
|
|
||||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
|
|
||||||
m.log("grimblast installed successfully to /usr/local/bin")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing niri from source...")
|
m.log("Installing niri from source...")
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, o.detectQuickshell())
|
dependencies = append(dependencies, o.detectQuickshell())
|
||||||
dependencies = append(dependencies, o.detectXDGPortal())
|
dependencies = append(dependencies, o.detectXDGPortal())
|
||||||
dependencies = append(dependencies, o.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, o.detectAccountsService())
|
dependencies = append(dependencies, o.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -82,7 +81,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
dependencies = append(dependencies, o.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, o.detectClipboardTools()...)
|
dependencies = append(dependencies, o.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -102,20 +100,6 @@ func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if o.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("rpm", "-q", pkg)
|
cmd := exec.Command("rpm", "-q", pkg)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
@@ -135,34 +119,59 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
|
||||||
|
|
||||||
// Manual builds
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
// Niri stable has native package support on openSUSE
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
niriVariant := variants["niri"]
|
||||||
|
packages["niri"] = o.getNiriMapping(niriVariant)
|
||||||
|
packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if o.commandExists("xwayland-satellite") {
|
if o.commandExists("xwayland-satellite") {
|
||||||
@@ -294,9 +303,23 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, obsPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
// Phase 2: System Packages (Zypper)
|
// Enable OBS repositories
|
||||||
|
if len(obsPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Enabling OBS repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: "Setting up OBS repositories for additional packages",
|
||||||
|
}
|
||||||
|
if err := o.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable OBS repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: System Packages (Zypper)
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -311,7 +334,22 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3: Manual Builds
|
// OBS Packages
|
||||||
|
obsPkgNames := o.extractPackageNames(obsPkgs)
|
||||||
|
if len(obsPkgNames) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.65,
|
||||||
|
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
|
}
|
||||||
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Builds
|
||||||
if len(manualPkgs) > 0 {
|
if len(manualPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -325,7 +363,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Configuration
|
// Configuration
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseConfiguration,
|
Phase: PhaseConfiguration,
|
||||||
Progress: 0.90,
|
Progress: 0.90,
|
||||||
@@ -334,7 +372,16 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: Complete
|
terminal := o.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := o.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.EnableDMSService(ctx); err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
Progress: 1.0,
|
Progress: 1.0,
|
||||||
@@ -346,8 +393,9 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) {
|
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
|
||||||
systemPkgs := []string{}
|
systemPkgs := []string{}
|
||||||
|
obsPkgs := []PackageMapping{}
|
||||||
manualPkgs := []string{}
|
manualPkgs := []string{}
|
||||||
|
|
||||||
variantMap := make(map[string]deps.PackageVariant)
|
variantMap := make(map[string]deps.PackageVariant)
|
||||||
@@ -375,12 +423,80 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
switch pkgInfo.Repository {
|
switch pkgInfo.Repository {
|
||||||
case RepoTypeSystem:
|
case RepoTypeSystem:
|
||||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||||
|
case RepoTypeOBS:
|
||||||
|
obsPkgs = append(obsPkgs, pkgInfo)
|
||||||
case RepoTypeManual:
|
case RepoTypeManual:
|
||||||
manualPkgs = append(manualPkgs, dep.Name)
|
manualPkgs = append(manualPkgs, dep.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
|
names := make([]string, len(packages))
|
||||||
|
for i, pkg := range packages {
|
||||||
|
names[i] = pkg.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, pkg := range obsPkgs {
|
||||||
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
|
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||||
|
|
||||||
|
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||||
|
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||||
|
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||||
|
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
|
||||||
|
repoPath, pkg.RepoURL)
|
||||||
|
|
||||||
|
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
||||||
|
if checkCmd.Run() == nil {
|
||||||
|
o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.20,
|
||||||
|
Step: fmt.Sprintf("Enabling OBS repo %s...", pkg.RepoURL),
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledRepos[pkg.RepoURL] = true
|
||||||
|
o.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh repositories with GPG auto-import
|
||||||
|
if len(enabledRepos) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: 0.25,
|
||||||
|
Step: "Refreshing repositories...",
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||||
|
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
|
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package distros
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -66,7 +64,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, u.detectQuickshell())
|
dependencies = append(dependencies, u.detectQuickshell())
|
||||||
dependencies = append(dependencies, u.detectXDGPortal())
|
dependencies = append(dependencies, u.detectXDGPortal())
|
||||||
dependencies = append(dependencies, u.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, u.detectAccountsService())
|
dependencies = append(dependencies, u.detectAccountsService())
|
||||||
|
|
||||||
// Hyprland-specific tools
|
// Hyprland-specific tools
|
||||||
@@ -82,7 +79,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
// Base detections (common across distros)
|
// Base detections (common across distros)
|
||||||
dependencies = append(dependencies, u.detectMatugen())
|
dependencies = append(dependencies, u.detectMatugen())
|
||||||
dependencies = append(dependencies, u.detectDgop())
|
dependencies = append(dependencies, u.detectDgop())
|
||||||
dependencies = append(dependencies, u.detectHyprpicker())
|
|
||||||
dependencies = append(dependencies, u.detectClipboardTools()...)
|
dependencies = append(dependencies, u.detectClipboardTools()...)
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
@@ -102,20 +98,6 @@ func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if u.packageInstalled("mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if u.commandExists("xwayland-satellite") {
|
if u.commandExists("xwayland-satellite") {
|
||||||
@@ -151,6 +133,10 @@ func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
|
return u.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||||
packages := map[string]PackageMapping{
|
packages := map[string]PackageMapping{
|
||||||
// Standard APT packages
|
// Standard APT packages
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
@@ -158,37 +144,60 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
|
|||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
|
|
||||||
|
|
||||||
// Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs)
|
// DMS packages from PPAs
|
||||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
// Use the cppiber PPA for Hyprland
|
// Use the cppiber PPA for Hyprland
|
||||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
|
||||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
|
||||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
|
||||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
niriVariant := variants["niri"]
|
||||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
packages["niri"] = u.getNiriMapping(niriVariant)
|
||||||
|
packages["xwayland-satellite"] = u.getXwaylandSatelliteMapping(niriVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "dms-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms-git"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "dms", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "quickshell", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "niri-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "niri", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
|
if variant == deps.VariantGit {
|
||||||
|
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhasePrerequisites,
|
Phase: PhasePrerequisites,
|
||||||
@@ -343,6 +352,15 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal := u.DetectTerminalFromDeps(dependencies)
|
||||||
|
if err := u.WriteEnvironmentConfig(terminal); err != nil {
|
||||||
|
u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.EnableDMSService(ctx); err != nil {
|
||||||
|
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 7: Complete
|
// Phase 7: Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
@@ -365,7 +383,7 @@ func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency,
|
|||||||
variantMap[dep.Name] = dep.Variant
|
variantMap[dep.Name] = dep.Variant
|
||||||
}
|
}
|
||||||
|
|
||||||
packageMap := u.GetPackageMapping(wm)
|
packageMap := u.GetPackageMappingWithVariants(wm, variantMap)
|
||||||
|
|
||||||
for _, dep := range dependencies {
|
for _, dep := range dependencies {
|
||||||
if disabledFlags[dep.Name] {
|
if disabledFlags[dep.Name] {
|
||||||
@@ -545,10 +563,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
buildDeps["libxcb1-dev"] = true
|
buildDeps["libxcb1-dev"] = true
|
||||||
buildDeps["libpipewire-0.3-dev"] = true
|
buildDeps["libpipewire-0.3-dev"] = true
|
||||||
buildDeps["libpam0g-dev"] = true
|
buildDeps["libpam0g-dev"] = true
|
||||||
case "ghostty":
|
|
||||||
buildDeps["curl"] = true
|
|
||||||
buildDeps["libgtk-4-dev"] = true
|
|
||||||
buildDeps["libadwaita-1-dev"] = true
|
|
||||||
case "matugen":
|
case "matugen":
|
||||||
buildDeps["curl"] = true
|
buildDeps["curl"] = true
|
||||||
case "cliphist":
|
case "cliphist":
|
||||||
@@ -562,10 +576,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "ghostty":
|
|
||||||
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Zig: %w", err)
|
|
||||||
}
|
|
||||||
case "cliphist", "dgop":
|
case "cliphist", "dgop":
|
||||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
@@ -629,40 +639,6 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
if u.commandExists("zig") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
|
|
||||||
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
|
|
||||||
|
|
||||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
|
|
||||||
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
|
|
||||||
return fmt.Errorf("failed to download Zig: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
extractCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
|
|
||||||
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
|
|
||||||
return fmt.Errorf("failed to extract Zig: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
linkCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
|
|
||||||
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if u.commandExists("go") {
|
if u.commandExists("go") {
|
||||||
return nil
|
return nil
|
||||||
@@ -710,30 +686,6 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
u.log("Installing Ghostty using Ubuntu installer script...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Running Ghostty Ubuntu installer...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
|
||||||
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
|
||||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
|
||||||
|
|
||||||
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u.log("Ghostty installed successfully using Ubuntu installer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -743,10 +695,6 @@ func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages
|
|||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
switch pkg {
|
switch pkg {
|
||||||
case "ghostty":
|
|
||||||
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
|||||||
switch dep.Name {
|
switch dep.Name {
|
||||||
case "dms (DankMaterialShell)", "quickshell":
|
case "dms (DankMaterialShell)", "quickshell":
|
||||||
categories["Shell"] = append(categories["Shell"], dep)
|
categories["Shell"] = append(categories["Shell"], dep)
|
||||||
case "hyprland", "grim", "slurp", "hyprctl", "grimblast":
|
case "hyprland", "hyprctl":
|
||||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||||
case "niri":
|
case "niri":
|
||||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||||
|
|||||||
@@ -333,35 +333,6 @@ func (n *NiriProvider) isRecentWindowsAction(action string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) parseSpawnArgs(s string) []string {
|
|
||||||
var args []string
|
|
||||||
var current strings.Builder
|
|
||||||
var inQuote, escaped bool
|
|
||||||
|
|
||||||
for _, r := range s {
|
|
||||||
switch {
|
|
||||||
case escaped:
|
|
||||||
current.WriteRune(r)
|
|
||||||
escaped = false
|
|
||||||
case r == '\\':
|
|
||||||
escaped = true
|
|
||||||
case r == '"':
|
|
||||||
inQuote = !inQuote
|
|
||||||
case r == ' ' && !inQuote:
|
|
||||||
if current.Len() > 0 {
|
|
||||||
args = append(args, current.String())
|
|
||||||
current.Reset()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
current.WriteRune(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if current.Len() > 0 {
|
|
||||||
args = append(args, current.String())
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
|
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
|
||||||
node := document.NewNode()
|
node := document.NewNode()
|
||||||
node.SetName(bind.Key)
|
node.SetName(bind.Key)
|
||||||
@@ -392,19 +363,62 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
|||||||
action = strings.TrimSpace(action)
|
action = strings.TrimSpace(action)
|
||||||
node := document.NewNode()
|
node := document.NewNode()
|
||||||
|
|
||||||
if !strings.HasPrefix(action, "spawn ") {
|
parts := n.parseActionParts(action)
|
||||||
|
if len(parts) == 0 {
|
||||||
node.SetName(action)
|
node.SetName(action)
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
node.SetName("spawn")
|
node.SetName(parts[0])
|
||||||
args := n.parseSpawnArgs(strings.TrimPrefix(action, "spawn "))
|
for _, arg := range parts[1:] {
|
||||||
for _, arg := range args {
|
if strings.Contains(arg, "=") {
|
||||||
|
kv := strings.SplitN(arg, "=", 2)
|
||||||
|
switch kv[1] {
|
||||||
|
case "true":
|
||||||
|
node.AddProperty(kv[0], true, "")
|
||||||
|
case "false":
|
||||||
|
node.AddProperty(kv[0], false, "")
|
||||||
|
default:
|
||||||
|
node.AddProperty(kv[0], kv[1], "")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
node.AddArgument(arg, "")
|
node.AddArgument(arg, "")
|
||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) parseActionParts(action string) []string {
|
||||||
|
var parts []string
|
||||||
|
var current strings.Builder
|
||||||
|
var inQuote, escaped, wasQuoted bool
|
||||||
|
|
||||||
|
for _, r := range action {
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
|
current.WriteRune(r)
|
||||||
|
escaped = false
|
||||||
|
case r == '\\':
|
||||||
|
escaped = true
|
||||||
|
case r == '"':
|
||||||
|
wasQuoted = true
|
||||||
|
inQuote = !inQuote
|
||||||
|
case r == ' ' && !inQuote:
|
||||||
|
if current.Len() > 0 || wasQuoted {
|
||||||
|
parts = append(parts, current.String())
|
||||||
|
current.Reset()
|
||||||
|
wasQuoted = false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
current.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current.Len() > 0 || wasQuoted {
|
||||||
|
parts = append(parts, current.String())
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
|
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
|
||||||
overridePath := n.GetOverridePath()
|
overridePath := n.GetOverridePath()
|
||||||
content := n.generateBindsContent(binds)
|
content := n.generateBindsContent(binds)
|
||||||
@@ -501,21 +515,50 @@ func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, in
|
|||||||
sb.WriteString(" { ")
|
sb.WriteString(" { ")
|
||||||
if len(node.Children) > 0 {
|
if len(node.Children) > 0 {
|
||||||
child := node.Children[0]
|
child := node.Children[0]
|
||||||
sb.WriteString(child.Name.String())
|
actionName := child.Name.String()
|
||||||
|
sb.WriteString(actionName)
|
||||||
|
forceQuote := actionName == "spawn"
|
||||||
for _, arg := range child.Arguments {
|
for _, arg := range child.Arguments {
|
||||||
sb.WriteString(" ")
|
sb.WriteString(" ")
|
||||||
n.writeQuotedArg(sb, arg.ValueString())
|
n.writeArg(sb, arg.ValueString(), forceQuote)
|
||||||
|
}
|
||||||
|
if child.Properties.Exist() {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(strings.TrimLeft(child.Properties.String(), " "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sb.WriteString("; }\n")
|
sb.WriteString("; }\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) writeQuotedArg(sb *strings.Builder, val string) {
|
func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) {
|
||||||
|
if !forceQuote && n.isNumericArg(val) {
|
||||||
|
sb.WriteString(val)
|
||||||
|
return
|
||||||
|
}
|
||||||
sb.WriteString("\"")
|
sb.WriteString("\"")
|
||||||
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
|
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
|
||||||
sb.WriteString("\"")
|
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 {
|
func (n *NiriProvider) validateBindsContent(content string) error {
|
||||||
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
|
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -265,6 +265,11 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
|
|||||||
for _, arg := range actionNode.Arguments {
|
for _, arg := range actionNode.Arguments {
|
||||||
args = append(args, arg.ValueString())
|
args = append(args, arg.ValueString())
|
||||||
}
|
}
|
||||||
|
if actionNode.Properties != nil {
|
||||||
|
if val, ok := actionNode.Properties.Get("focus"); ok {
|
||||||
|
args = append(args, "focus="+val.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var description string
|
var description string
|
||||||
|
|||||||
@@ -496,3 +496,135 @@ func TestNiriParseMultipleArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
||||||
|
Mod+2 hotkey-overlay-title="Focus Workspace 2" { focus-workspace 2; }
|
||||||
|
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
||||||
|
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 4 {
|
||||||
|
t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch kb.Key {
|
||||||
|
case "1":
|
||||||
|
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" {
|
||||||
|
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "1" {
|
||||||
|
t.Errorf("Mod+1 action/args mismatch: %+v", kb)
|
||||||
|
}
|
||||||
|
if kb.Description != "Focus Workspace 1" {
|
||||||
|
t.Errorf("Mod+1 description = %q, want 'Focus Workspace 1'", kb.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "0":
|
||||||
|
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "10" {
|
||||||
|
t.Errorf("Mod+0 action/args mismatch: %+v", kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseQuotedStringArgs(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
||||||
|
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
||||||
|
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
if kb.Action == "set-column-width" {
|
||||||
|
if len(kb.Args) != 1 {
|
||||||
|
t.Errorf("set-column-width should have 1 arg, got %d", len(kb.Args))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kb.Args[0] != "-10%" && kb.Args[0] != "+10%" {
|
||||||
|
t.Errorf("set-column-width arg = %q, want -10%% or +10%%", kb.Args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriParseActionWithProperties(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|
||||||
|
content := `binds {
|
||||||
|
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; }
|
||||||
|
Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; }
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 3 {
|
||||||
|
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch kb.Action {
|
||||||
|
case "move-column-to-workspace":
|
||||||
|
if len(kb.Args) != 2 {
|
||||||
|
t.Errorf("move-column-to-workspace should have 2 args (index + focus), got %d", len(kb.Args))
|
||||||
|
}
|
||||||
|
hasIndex := false
|
||||||
|
hasFocus := false
|
||||||
|
for _, arg := range kb.Args {
|
||||||
|
if arg == "1" || arg == "2" {
|
||||||
|
hasIndex = true
|
||||||
|
}
|
||||||
|
if arg == "focus=false" {
|
||||||
|
hasFocus = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasIndex {
|
||||||
|
t.Errorf("move-column-to-workspace missing index arg")
|
||||||
|
}
|
||||||
|
if !hasFocus {
|
||||||
|
t.Errorf("move-column-to-workspace missing focus=false arg")
|
||||||
|
}
|
||||||
|
case "next-window":
|
||||||
|
if kb.Key != "Tab" {
|
||||||
|
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -397,3 +397,211 @@ recent-windows {
|
|||||||
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
|
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
binds map[string]*overrideBind
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "workspace with numeric arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+1": {
|
||||||
|
Key: "Mod+1",
|
||||||
|
Action: "focus-workspace 1",
|
||||||
|
Description: "Focus Workspace 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "workspace with large numeric arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Mod+0": {
|
||||||
|
Key: "Mod+0",
|
||||||
|
Action: "focus-workspace 10",
|
||||||
|
Description: "Focus Workspace 10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "percentage string arg (should be quoted)",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Super+Minus": {
|
||||||
|
Key: "Super+Minus",
|
||||||
|
Action: `set-column-width "-10%"`,
|
||||||
|
Description: "Adjust Column Width -10%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positive percentage string arg",
|
||||||
|
binds: map[string]*overrideBind{
|
||||||
|
"Super+Equal": {
|
||||||
|
Key: "Super+Equal",
|
||||||
|
Action: `set-column-width "+10%"`,
|
||||||
|
Description: "Adjust Column Width +10%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `binds {
|
||||||
|
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := provider.generateBindsContent(tt.binds)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"Super+Equal": {
|
||||||
|
Key: "Super+Equal",
|
||||||
|
Action: "set-window-height +10%",
|
||||||
|
Description: "Adjust Window Height +10%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
expected := `binds {
|
||||||
|
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if content != expected {
|
||||||
|
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"XF86AudioLowerVolume": {
|
||||||
|
Key: "XF86AudioLowerVolume",
|
||||||
|
Action: `spawn "dms" "ipc" "call" "audio" "decrement" "3"`,
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
expected := `binds {
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if content != expected {
|
||||||
|
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"XF86AudioLowerVolume": {
|
||||||
|
Key: "XF86AudioLowerVolume",
|
||||||
|
Action: "spawn dms ipc call audio decrement 3",
|
||||||
|
Options: map[string]any{"allow-when-locked": true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
expected := `binds {
|
||||||
|
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if content != expected {
|
||||||
|
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
|
||||||
|
provider := NewNiriProvider("")
|
||||||
|
|
||||||
|
binds := map[string]*overrideBind{
|
||||||
|
"Mod+1": {
|
||||||
|
Key: "Mod+1",
|
||||||
|
Action: "focus-workspace 1",
|
||||||
|
Description: "Focus Workspace 1",
|
||||||
|
},
|
||||||
|
"Mod+2": {
|
||||||
|
Key: "Mod+2",
|
||||||
|
Action: "focus-workspace 2",
|
||||||
|
Description: "Focus Workspace 2",
|
||||||
|
},
|
||||||
|
"Mod+Shift+1": {
|
||||||
|
Key: "Mod+Shift+1",
|
||||||
|
Action: "move-column-to-workspace 1",
|
||||||
|
Description: "Move to Workspace 1",
|
||||||
|
},
|
||||||
|
"Super+Minus": {
|
||||||
|
Key: "Super+Minus",
|
||||||
|
Action: "set-column-width -10%",
|
||||||
|
Description: "Adjust Column Width -10%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
content := provider.generateBindsContent(binds)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Section.Keybinds) != 4 {
|
||||||
|
t.Errorf("Expected 4 keybinds after round-trip, got %d", len(result.Section.Keybinds))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundFocusWS1 := false
|
||||||
|
foundMoveWS1 := false
|
||||||
|
foundSetWidth := false
|
||||||
|
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
switch {
|
||||||
|
case kb.Action == "focus-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
|
||||||
|
foundFocusWS1 = true
|
||||||
|
case kb.Action == "move-column-to-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
|
||||||
|
foundMoveWS1 = true
|
||||||
|
case kb.Action == "set-column-width" && len(kb.Args) > 0 && kb.Args[0] == "-10%":
|
||||||
|
foundSetWidth = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundFocusWS1 {
|
||||||
|
t.Error("focus-workspace 1 not found after round-trip")
|
||||||
|
}
|
||||||
|
if !foundMoveWS1 {
|
||||||
|
t.Error("move-column-to-workspace 1 not found after round-trip")
|
||||||
|
}
|
||||||
|
if !foundSetWidth {
|
||||||
|
t.Error("set-column-width -10% not found after round-trip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
587
core/internal/matugen/matugen.go
Normal file
587
core/internal/matugen/matugen.go
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
package matugen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
matugenVersionOnce sync.Once
|
||||||
|
matugenSupportsCOE bool
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
StateDir string
|
||||||
|
ShellDir string
|
||||||
|
ConfigDir string
|
||||||
|
Kind string
|
||||||
|
Value string
|
||||||
|
Mode string
|
||||||
|
IconTheme string
|
||||||
|
MatugenType string
|
||||||
|
RunUserTemplates bool
|
||||||
|
StockColors string
|
||||||
|
SyncModeWithPortal bool
|
||||||
|
TerminalsAlwaysDark bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColorsOutput struct {
|
||||||
|
Colors struct {
|
||||||
|
Dark map[string]string `json:"dark"`
|
||||||
|
Light map[string]string `json:"light"`
|
||||||
|
} `json:"colors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Options) ColorsOutput() string {
|
||||||
|
return filepath.Join(o.StateDir, "dms-colors.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(opts Options) error {
|
||||||
|
if opts.StateDir == "" {
|
||||||
|
return fmt.Errorf("state-dir is required")
|
||||||
|
}
|
||||||
|
if opts.ShellDir == "" {
|
||||||
|
return fmt.Errorf("shell-dir is required")
|
||||||
|
}
|
||||||
|
if opts.ConfigDir == "" {
|
||||||
|
return fmt.Errorf("config-dir is required")
|
||||||
|
}
|
||||||
|
if opts.Kind == "" {
|
||||||
|
return fmt.Errorf("kind is required")
|
||||||
|
}
|
||||||
|
if opts.Value == "" {
|
||||||
|
return fmt.Errorf("value is required")
|
||||||
|
}
|
||||||
|
if opts.Mode == "" {
|
||||||
|
opts.Mode = "dark"
|
||||||
|
}
|
||||||
|
if opts.MatugenType == "" {
|
||||||
|
opts.MatugenType = "scheme-tonal-spot"
|
||||||
|
}
|
||||||
|
if opts.IconTheme == "" {
|
||||||
|
opts.IconTheme = "System Default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create state dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
||||||
|
|
||||||
|
if err := buildOnce(&opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.SyncModeWithPortal {
|
||||||
|
syncColorScheme(opts.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Done")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOnce(opts *Options) error {
|
||||||
|
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp config: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to build config: %w", err)
|
||||||
|
}
|
||||||
|
cfgFile.Close()
|
||||||
|
|
||||||
|
var primaryDark, primaryLight, surface string
|
||||||
|
var dank16JSON string
|
||||||
|
var importArgs []string
|
||||||
|
|
||||||
|
if opts.StockColors != "" {
|
||||||
|
log.Info("Using stock/custom theme colors with matugen base")
|
||||||
|
primaryDark = extractNestedColor(opts.StockColors, "primary", "dark")
|
||||||
|
primaryLight = extractNestedColor(opts.StockColors, "primary", "light")
|
||||||
|
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||||
|
|
||||||
|
if primaryDark == "" {
|
||||||
|
return fmt.Errorf("failed to extract primary dark from stock colors")
|
||||||
|
}
|
||||||
|
if primaryLight == "" {
|
||||||
|
primaryLight = primaryDark
|
||||||
|
}
|
||||||
|
|
||||||
|
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||||
|
importData := fmt.Sprintf(`{"colors": %s, "dank16": %s}`, opts.StockColors, dank16JSON)
|
||||||
|
importArgs = []string{"--import-json-string", importData}
|
||||||
|
|
||||||
|
log.Info("Running matugen color hex with stock color overrides")
|
||||||
|
args := []string{"color", "hex", primaryDark, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
|
args = append(args, importArgs...)
|
||||||
|
if err := runMatugen(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||||
|
|
||||||
|
matJSON, err := runMatugenDryRun(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("matugen dry-run failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||||
|
primaryLight = extractMatugenColor(matJSON, "primary", "light")
|
||||||
|
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||||
|
|
||||||
|
if primaryDark == "" {
|
||||||
|
return fmt.Errorf("failed to extract primary color")
|
||||||
|
}
|
||||||
|
if primaryLight == "" {
|
||||||
|
primaryLight = primaryDark
|
||||||
|
}
|
||||||
|
|
||||||
|
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||||
|
importData := fmt.Sprintf(`{"dank16": %s}`, dank16JSON)
|
||||||
|
importArgs = []string{"--import-json-string", importData}
|
||||||
|
|
||||||
|
log.Infof("Running matugen %s with dank16 injection", opts.Kind)
|
||||||
|
var args []string
|
||||||
|
switch opts.Kind {
|
||||||
|
case "hex":
|
||||||
|
args = []string{"color", "hex", opts.Value}
|
||||||
|
default:
|
||||||
|
args = []string{opts.Kind, opts.Value}
|
||||||
|
}
|
||||||
|
args = append(args, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
|
args = append(args, importArgs...)
|
||||||
|
if err := runMatugen(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshGTK(opts.ConfigDir, opts.Mode)
|
||||||
|
signalTerminals()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||||
|
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||||
|
|
||||||
|
wroteConfig := false
|
||||||
|
if opts.RunUserTemplates {
|
||||||
|
if data, err := os.ReadFile(userConfigPath); err == nil {
|
||||||
|
configSection := extractTOMLSection(string(data), "[config]", "[templates]")
|
||||||
|
if configSection != "" {
|
||||||
|
cfgFile.WriteString(configSection)
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
wroteConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wroteConfig {
|
||||||
|
cfgFile.WriteString("[config]\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfigPath := filepath.Join(opts.ShellDir, "matugen", "configs", "base.toml")
|
||||||
|
if data, err := os.ReadFile(baseConfigPath); err == nil {
|
||||||
|
content := string(data)
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) == "[config]" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfgFile.WriteString(substituteShellDir(line, opts.ShellDir) + "\n")
|
||||||
|
}
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(cfgFile, `[templates.dank]
|
||||||
|
input_path = '%s/matugen/templates/dank.json'
|
||||||
|
output_path = '%s'
|
||||||
|
|
||||||
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
switch opts.Mode {
|
||||||
|
case "light":
|
||||||
|
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
|
||||||
|
default:
|
||||||
|
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, "niri", "niri.toml")
|
||||||
|
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
|
||||||
|
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
|
||||||
|
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
|
||||||
|
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
|
||||||
|
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
|
||||||
|
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
|
||||||
|
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
|
||||||
|
|
||||||
|
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
|
||||||
|
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
|
||||||
|
|
||||||
|
if opts.RunUserTemplates {
|
||||||
|
if data, err := os.ReadFile(userConfigPath); err == nil {
|
||||||
|
templatesSection := extractTOMLSection(string(data), "[templates]", "")
|
||||||
|
if templatesSection != "" {
|
||||||
|
cfgFile.WriteString(templatesSection)
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userPluginConfigDir := filepath.Join(opts.ConfigDir, "matugen", "dms", "configs")
|
||||||
|
if entries, err := os.ReadDir(userPluginConfigDir); err == nil {
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !strings.HasSuffix(entry.Name(), ".toml") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data, err := os.ReadFile(filepath.Join(userPluginConfigDir, entry.Name())); err == nil {
|
||||||
|
cfgFile.WriteString(string(data))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
|
||||||
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if checkCmd != "skip" && !commandExists(checkCmd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfgFile.WriteString(substituteShellDir(string(data), opts.ShellDir))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fileName string) {
|
||||||
|
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||||
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if checkCmd != "skip" && !commandExists(checkCmd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
if !opts.TerminalsAlwaysDark {
|
||||||
|
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if !strings.Contains(line, "input_path") || !strings.Contains(line, "SHELL_DIR/matugen/templates/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
start := strings.Index(line, "'SHELL_DIR/matugen/templates/")
|
||||||
|
if start == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end := strings.Index(line[start+1:], "'")
|
||||||
|
if end == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
templateName := line[start+len("'SHELL_DIR/matugen/templates/") : start+1+end]
|
||||||
|
origPath := filepath.Join(opts.ShellDir, "matugen", "templates", templateName)
|
||||||
|
|
||||||
|
origData, err := os.ReadFile(origPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
||||||
|
tmpPath := filepath.Join(tmpDir, templateName)
|
||||||
|
if err := os.WriteFile(tmpPath, []byte(modified), 0644); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
content = strings.ReplaceAll(content,
|
||||||
|
fmt.Sprintf("'SHELL_DIR/matugen/templates/%s'", templateName),
|
||||||
|
fmt.Sprintf("'%s'", tmpPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
|
||||||
|
cfgFile.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
|
||||||
|
if _, err := os.Stat(extDir); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateDir := filepath.Join(shellDir, "matugen", "templates")
|
||||||
|
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
|
||||||
|
input_path = '%s/vscode-color-theme-default.json'
|
||||||
|
output_path = '%s/themes/dankshell-default.json'
|
||||||
|
|
||||||
|
[templates.dms%sdark]
|
||||||
|
input_path = '%s/vscode-color-theme-dark.json'
|
||||||
|
output_path = '%s/themes/dankshell-dark.json'
|
||||||
|
|
||||||
|
[templates.dms%slight]
|
||||||
|
input_path = '%s/vscode-color-theme-light.json'
|
||||||
|
output_path = '%s/themes/dankshell-light.json'
|
||||||
|
|
||||||
|
`, name, templateDir, extDir,
|
||||||
|
name, templateDir, extDir,
|
||||||
|
name, templateDir, extDir)
|
||||||
|
log.Infof("Added %s theme config (extension found at %s)", name, extDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func substituteShellDir(content, shellDir string) string {
|
||||||
|
return strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTOMLSection(content, startMarker, endMarker string) string {
|
||||||
|
startIdx := strings.Index(content, startMarker)
|
||||||
|
if startIdx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if endMarker == "" {
|
||||||
|
return content[startIdx:]
|
||||||
|
}
|
||||||
|
|
||||||
|
endIdx := strings.Index(content[startIdx:], endMarker)
|
||||||
|
if endIdx == -1 {
|
||||||
|
return content[startIdx:]
|
||||||
|
}
|
||||||
|
return content[startIdx : startIdx+endIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandExists(name string) bool {
|
||||||
|
_, err := exec.LookPath(name)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMatugenVersion() {
|
||||||
|
matugenVersionOnce.Do(func() {
|
||||||
|
cmd := exec.Command("matugen", "--version")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStr := strings.TrimSpace(string(output))
|
||||||
|
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
||||||
|
|
||||||
|
parts := strings.Split(versionStr, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
minor, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
||||||
|
if matugenSupportsCOE {
|
||||||
|
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugen(args []string) error {
|
||||||
|
checkMatugenVersion()
|
||||||
|
|
||||||
|
if matugenSupportsCOE {
|
||||||
|
args = append([]string{"--continue-on-error"}, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("matugen", args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugenDryRun(opts *Options) (string, error) {
|
||||||
|
var args []string
|
||||||
|
switch opts.Kind {
|
||||||
|
case "hex":
|
||||||
|
args = []string{"color", "hex", opts.Value}
|
||||||
|
default:
|
||||||
|
args = []string{opts.Kind, opts.Value}
|
||||||
|
}
|
||||||
|
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||||
|
|
||||||
|
cmd := exec.Command("matugen", args...)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(string(output), "\n", ""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractMatugenColor(jsonStr, colorName, variant string) string {
|
||||||
|
var data map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
colors, ok := data["colors"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
colorData, ok := colors[colorName].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variantData, ok := colorData[variant].(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return variantData
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractNestedColor(jsonStr, colorName, variant string) string {
|
||||||
|
var data map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
colorData, ok := data[colorName].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variantData, ok := colorData[variant].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
color, ok := variantData["color"].(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateDank16Variants(primaryDark, primaryLight, surface, mode string) string {
|
||||||
|
variantOpts := dank16.VariantOptions{
|
||||||
|
PrimaryDark: primaryDark,
|
||||||
|
PrimaryLight: primaryLight,
|
||||||
|
Background: surface,
|
||||||
|
UseDPS: true,
|
||||||
|
IsLightMode: mode == "light",
|
||||||
|
}
|
||||||
|
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||||
|
return dank16.GenerateVariantJSON(variantColors)
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshGTK(configDir, mode string) {
|
||||||
|
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
||||||
|
|
||||||
|
info, err := os.Lstat(gtkCSS)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRun := false
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
target, err := os.Readlink(gtkCSS)
|
||||||
|
if err == nil && strings.Contains(target, "dank-colors.css") {
|
||||||
|
shouldRun = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err := os.ReadFile(gtkCSS)
|
||||||
|
if err == nil && strings.Contains(string(data), "dank-colors.css") {
|
||||||
|
shouldRun = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldRun {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
||||||
|
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalTerminals() {
|
||||||
|
signalByName("kitty", syscall.SIGUSR1)
|
||||||
|
signalByName("ghostty", syscall.SIGUSR2)
|
||||||
|
signalByName(".kitty-wrapped", syscall.SIGUSR1)
|
||||||
|
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalByName(name string, sig syscall.Signal) {
|
||||||
|
entries, err := os.ReadDir("/proc")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
pid, err := strconv.Atoi(entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(comm)) == name {
|
||||||
|
syscall.Kill(pid, sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncColorScheme(mode string) {
|
||||||
|
scheme := "prefer-dark"
|
||||||
|
if mode == "light" {
|
||||||
|
scheme = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
|
||||||
|
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
139
core/internal/matugen/queue.go
Normal file
139
core/internal/matugen/queue.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package matugen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueuedJob struct {
|
||||||
|
Options Options
|
||||||
|
Done chan Result
|
||||||
|
Ctx context.Context
|
||||||
|
Cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queue struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
current *QueuedJob
|
||||||
|
pending *QueuedJob
|
||||||
|
jobDone chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalQueue *Queue
|
||||||
|
var queueOnce sync.Once
|
||||||
|
|
||||||
|
func GetQueue() *Queue {
|
||||||
|
queueOnce.Do(func() {
|
||||||
|
globalQueue = &Queue{
|
||||||
|
jobDone: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return globalQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) Submit(opts Options) <-chan Result {
|
||||||
|
result := make(chan Result, 1)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
job := &QueuedJob{
|
||||||
|
Options: opts,
|
||||||
|
Done: result,
|
||||||
|
Ctx: ctx,
|
||||||
|
Cancel: cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
q.mu.Lock()
|
||||||
|
|
||||||
|
if q.pending != nil {
|
||||||
|
log.Info("Cancelling pending theme request")
|
||||||
|
q.pending.Cancel()
|
||||||
|
q.pending.Done <- Result{Success: false, Error: context.Canceled}
|
||||||
|
close(q.pending.Done)
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.current != nil {
|
||||||
|
q.pending = job
|
||||||
|
q.mu.Unlock()
|
||||||
|
log.Info("Theme request queued (worker running)")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
q.current = job
|
||||||
|
q.mu.Unlock()
|
||||||
|
|
||||||
|
go q.runWorker()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) runWorker() {
|
||||||
|
for {
|
||||||
|
q.mu.Lock()
|
||||||
|
job := q.current
|
||||||
|
if job == nil {
|
||||||
|
q.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q.mu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-job.Ctx.Done():
|
||||||
|
q.finishJob(Result{Success: false, Error: context.Canceled})
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Processing theme: %s %s (%s)", job.Options.Kind, job.Options.Value, job.Options.Mode)
|
||||||
|
err := Run(job.Options)
|
||||||
|
|
||||||
|
var result Result
|
||||||
|
if err != nil {
|
||||||
|
result = Result{Success: false, Error: err}
|
||||||
|
} else {
|
||||||
|
result = Result{Success: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
q.finishJob(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) finishJob(result Result) {
|
||||||
|
q.mu.Lock()
|
||||||
|
defer q.mu.Unlock()
|
||||||
|
|
||||||
|
if q.current != nil {
|
||||||
|
select {
|
||||||
|
case q.current.Done <- result:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
close(q.current.Done)
|
||||||
|
}
|
||||||
|
|
||||||
|
q.current = q.pending
|
||||||
|
q.pending = nil
|
||||||
|
|
||||||
|
if q.current == nil {
|
||||||
|
select {
|
||||||
|
case q.jobDone <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) IsRunning() bool {
|
||||||
|
q.mu.Lock()
|
||||||
|
defer q.mu.Unlock()
|
||||||
|
return q.current != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) HasPending() bool {
|
||||||
|
q.mu.Lock()
|
||||||
|
defer q.mu.Unlock()
|
||||||
|
return q.pending != nil
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ func NewZdwlIpcManagerV2(ctx *client.Context) *ZdwlIpcManagerV2 {
|
|||||||
// Indicates that the client will not the dwl_ipc_manager object anymore.
|
// Indicates that the client will not the dwl_ipc_manager object anymore.
|
||||||
// Objects created through this instance are not affected.
|
// Objects created through this instance are not affected.
|
||||||
func (i *ZdwlIpcManagerV2) Release() error {
|
func (i *ZdwlIpcManagerV2) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -188,7 +188,7 @@ func NewZdwlIpcOutputV2(ctx *client.Context) *ZdwlIpcOutputV2 {
|
|||||||
//
|
//
|
||||||
// Indicates to that the client no longer needs this dwl_ipc_output.
|
// Indicates to that the client no longer needs this dwl_ipc_output.
|
||||||
func (i *ZdwlIpcOutputV2) Release() error {
|
func (i *ZdwlIpcOutputV2) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func (i *ExtWorkspaceManagerV1) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ExtWorkspaceManagerV1) Destroy() error {
|
func (i *ExtWorkspaceManagerV1) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ func (i *ExtWorkspaceGroupHandleV1) CreateWorkspace(workspace string) error {
|
|||||||
// use the workspace group object any more or after the removed event to finalize
|
// use the workspace group object any more or after the removed event to finalize
|
||||||
// the destruction of the object.
|
// the destruction of the object.
|
||||||
func (i *ExtWorkspaceGroupHandleV1) Destroy() error {
|
func (i *ExtWorkspaceGroupHandleV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -655,7 +655,7 @@ func NewExtWorkspaceHandleV1(ctx *client.Context) *ExtWorkspaceHandleV1 {
|
|||||||
// use the workspace object any more or after the remove event to finalize
|
// use the workspace object any more or after the remove event to finalize
|
||||||
// the destruction of the object.
|
// the destruction of the object.
|
||||||
func (i *ExtWorkspaceHandleV1) Destroy() error {
|
func (i *ExtWorkspaceHandleV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : internal/proto/xml/keyboard-shortcuts-inhibit-unstable-v1.xml
|
||||||
|
//
|
||||||
|
// keyboard_shortcuts_inhibit_unstable_v1 Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2017 Red Hat Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
// copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice (including the next
|
||||||
|
// paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
// Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package keyboard_shortcuts_inhibit
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwpKeyboardShortcutsInhibitManagerV1InterfaceName = "zwp_keyboard_shortcuts_inhibit_manager_v1"
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
|
||||||
|
//
|
||||||
|
// A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||||
|
type ZwpKeyboardShortcutsInhibitManagerV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwpKeyboardShortcutsInhibitManagerV1 : context object for keyboard grab_manager
|
||||||
|
//
|
||||||
|
// A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||||
|
func NewZwpKeyboardShortcutsInhibitManagerV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitManagerV1 {
|
||||||
|
zwpKeyboardShortcutsInhibitManagerV1 := &ZwpKeyboardShortcutsInhibitManagerV1{}
|
||||||
|
ctx.Register(zwpKeyboardShortcutsInhibitManagerV1)
|
||||||
|
return zwpKeyboardShortcutsInhibitManagerV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the keyboard shortcuts inhibitor object
|
||||||
|
//
|
||||||
|
// Destroy the keyboard shortcuts inhibitor manager.
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitManagerV1) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// InhibitShortcuts : create a new keyboard shortcuts inhibitor object
|
||||||
|
//
|
||||||
|
// Create a new keyboard shortcuts inhibitor object associated with
|
||||||
|
// the given surface for the given seat.
|
||||||
|
//
|
||||||
|
// If shortcuts are already inhibited for the specified seat and surface,
|
||||||
|
// a protocol error "already_inhibited" is raised by the compositor.
|
||||||
|
//
|
||||||
|
// surface: the surface that inhibits the keyboard shortcuts behavior
|
||||||
|
// seat: the wl_seat for which keyboard shortcuts should be disabled
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitManagerV1) InhibitShortcuts(surface *client.Surface, seat *client.Seat) (*ZwpKeyboardShortcutsInhibitorV1, error) {
|
||||||
|
id := NewZwpKeyboardShortcutsInhibitorV1(i.Context())
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], seat.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwpKeyboardShortcutsInhibitManagerV1Error uint32
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1Error :
|
||||||
|
const (
|
||||||
|
// ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited : the shortcuts are already inhibited for this surface
|
||||||
|
ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited ZwpKeyboardShortcutsInhibitManagerV1Error = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
|
||||||
|
return "already_inhibited"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwpKeyboardShortcutsInhibitManagerV1ErrorAlreadyInhibited:
|
||||||
|
return "0"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwpKeyboardShortcutsInhibitManagerV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwpKeyboardShortcutsInhibitorV1InterfaceName = "zwp_keyboard_shortcuts_inhibitor_v1"
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
|
||||||
|
//
|
||||||
|
// A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||||
|
// its own keyboard shortcuts when the associated surface has keyboard
|
||||||
|
// focus. As a result, when the surface has keyboard focus on the given
|
||||||
|
// seat, it will receive all key events originating from the specified
|
||||||
|
// seat, even those which would normally be caught by the compositor for
|
||||||
|
// its own shortcuts.
|
||||||
|
//
|
||||||
|
// The Wayland compositor is however under no obligation to disable
|
||||||
|
// all of its shortcuts, and may keep some special key combo for its own
|
||||||
|
// use, including but not limited to one allowing the user to forcibly
|
||||||
|
// restore normal keyboard events routing in the case of an unwilling
|
||||||
|
// client. The compositor may also use the same key combo to reactivate
|
||||||
|
// an existing shortcut inhibitor that was previously deactivated on
|
||||||
|
// user request.
|
||||||
|
//
|
||||||
|
// When the compositor restores its own keyboard shortcuts, an
|
||||||
|
// "inactive" event is emitted to notify the client that the keyboard
|
||||||
|
// shortcuts inhibitor is not effectively active for the surface and
|
||||||
|
// seat any more, and the client should not expect to receive all
|
||||||
|
// keyboard events.
|
||||||
|
//
|
||||||
|
// When the keyboard shortcuts inhibitor is inactive, the client has
|
||||||
|
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||||
|
//
|
||||||
|
// The user can chose to re-enable a previously deactivated keyboard
|
||||||
|
// shortcuts inhibitor using any mechanism the compositor may offer,
|
||||||
|
// in which case the compositor will send an "active" event to notify
|
||||||
|
// the client.
|
||||||
|
//
|
||||||
|
// If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||||
|
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||||
|
// compositor will restore its own keyboard shortcuts but no "inactive"
|
||||||
|
// event is emitted in this case.
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
activeHandler ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc
|
||||||
|
inactiveHandler ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwpKeyboardShortcutsInhibitorV1 : context object for keyboard shortcuts inhibitor
|
||||||
|
//
|
||||||
|
// A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||||
|
// its own keyboard shortcuts when the associated surface has keyboard
|
||||||
|
// focus. As a result, when the surface has keyboard focus on the given
|
||||||
|
// seat, it will receive all key events originating from the specified
|
||||||
|
// seat, even those which would normally be caught by the compositor for
|
||||||
|
// its own shortcuts.
|
||||||
|
//
|
||||||
|
// The Wayland compositor is however under no obligation to disable
|
||||||
|
// all of its shortcuts, and may keep some special key combo for its own
|
||||||
|
// use, including but not limited to one allowing the user to forcibly
|
||||||
|
// restore normal keyboard events routing in the case of an unwilling
|
||||||
|
// client. The compositor may also use the same key combo to reactivate
|
||||||
|
// an existing shortcut inhibitor that was previously deactivated on
|
||||||
|
// user request.
|
||||||
|
//
|
||||||
|
// When the compositor restores its own keyboard shortcuts, an
|
||||||
|
// "inactive" event is emitted to notify the client that the keyboard
|
||||||
|
// shortcuts inhibitor is not effectively active for the surface and
|
||||||
|
// seat any more, and the client should not expect to receive all
|
||||||
|
// keyboard events.
|
||||||
|
//
|
||||||
|
// When the keyboard shortcuts inhibitor is inactive, the client has
|
||||||
|
// no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||||
|
//
|
||||||
|
// The user can chose to re-enable a previously deactivated keyboard
|
||||||
|
// shortcuts inhibitor using any mechanism the compositor may offer,
|
||||||
|
// in which case the compositor will send an "active" event to notify
|
||||||
|
// the client.
|
||||||
|
//
|
||||||
|
// If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||||
|
// focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||||
|
// compositor will restore its own keyboard shortcuts but no "inactive"
|
||||||
|
// event is emitted in this case.
|
||||||
|
func NewZwpKeyboardShortcutsInhibitorV1(ctx *client.Context) *ZwpKeyboardShortcutsInhibitorV1 {
|
||||||
|
zwpKeyboardShortcutsInhibitorV1 := &ZwpKeyboardShortcutsInhibitorV1{}
|
||||||
|
ctx.Register(zwpKeyboardShortcutsInhibitorV1)
|
||||||
|
return zwpKeyboardShortcutsInhibitorV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the keyboard shortcuts inhibitor object
|
||||||
|
//
|
||||||
|
// Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1ActiveEvent : shortcuts are inhibited
|
||||||
|
//
|
||||||
|
// This event indicates that the shortcut inhibitor is active.
|
||||||
|
//
|
||||||
|
// The compositor sends this event every time compositor shortcuts
|
||||||
|
// are inhibited on behalf of the surface. When active, the client
|
||||||
|
// may receive input events normally reserved by the compositor
|
||||||
|
// (see zwp_keyboard_shortcuts_inhibitor_v1).
|
||||||
|
//
|
||||||
|
// This occurs typically when the initial request "inhibit_shortcuts"
|
||||||
|
// first becomes active or when the user instructs the compositor to
|
||||||
|
// re-enable and existing shortcuts inhibitor using any mechanism
|
||||||
|
// offered by the compositor.
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1ActiveEvent struct{}
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1ActiveEvent)
|
||||||
|
|
||||||
|
// SetActiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1ActiveEvent
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) SetActiveHandler(f ZwpKeyboardShortcutsInhibitorV1ActiveHandlerFunc) {
|
||||||
|
i.activeHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwpKeyboardShortcutsInhibitorV1InactiveEvent : shortcuts are restored
|
||||||
|
//
|
||||||
|
// This event indicates that the shortcuts inhibitor is inactive,
|
||||||
|
// normal shortcuts processing is restored by the compositor.
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1InactiveEvent struct{}
|
||||||
|
type ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc func(ZwpKeyboardShortcutsInhibitorV1InactiveEvent)
|
||||||
|
|
||||||
|
// SetInactiveHandler : sets handler for ZwpKeyboardShortcutsInhibitorV1InactiveEvent
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) SetInactiveHandler(f ZwpKeyboardShortcutsInhibitorV1InactiveHandlerFunc) {
|
||||||
|
i.inactiveHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ZwpKeyboardShortcutsInhibitorV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||||
|
switch opcode {
|
||||||
|
case 0:
|
||||||
|
if i.activeHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwpKeyboardShortcutsInhibitorV1ActiveEvent
|
||||||
|
|
||||||
|
i.activeHandler(e)
|
||||||
|
case 1:
|
||||||
|
if i.inactiveHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwpKeyboardShortcutsInhibitorV1InactiveEvent
|
||||||
|
|
||||||
|
i.inactiveHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ func (i *ZwlrGammaControlManagerV1) GetGammaControl(output *client.Output) (*Zwl
|
|||||||
// All objects created by the manager will still remain valid, until their
|
// All objects created by the manager will still remain valid, until their
|
||||||
// appropriate destroy request has been called.
|
// appropriate destroy request has been called.
|
||||||
func (i *ZwlrGammaControlManagerV1) Destroy() error {
|
func (i *ZwlrGammaControlManagerV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -169,7 +169,7 @@ func (i *ZwlrGammaControlV1) SetGamma(fd int) error {
|
|||||||
// Destroys the gamma control object. If the object is still valid, this
|
// Destroys the gamma control object. If the object is still valid, this
|
||||||
// restores the original gamma tables.
|
// restores the original gamma tables.
|
||||||
func (i *ZwlrGammaControlV1) Destroy() error {
|
func (i *ZwlrGammaControlV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
792
core/internal/proto/wlr_layer_shell/layer_shell.go
Normal file
792
core/internal/proto/wlr_layer_shell/layer_shell.go
Normal file
@@ -0,0 +1,792 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : internal/proto/xml/wlr-layer-shell-unstable-v1.xml
|
||||||
|
//
|
||||||
|
// wlr_layer_shell_unstable_v1 Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2017 Drew DeVault
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, distribute, and sell this
|
||||||
|
// software and its documentation for any purpose is hereby granted
|
||||||
|
// without fee, provided that the above copyright notice appear in
|
||||||
|
// all copies and that both that copyright notice and this permission
|
||||||
|
// notice appear in supporting documentation, and that the name of
|
||||||
|
// the copyright holders not be used in advertising or publicity
|
||||||
|
// pertaining to distribution of the software without specific,
|
||||||
|
// written prior permission. The copyright holders make no
|
||||||
|
// representations about the suitability of this software for any
|
||||||
|
// purpose. It is provided "as is" without express or implied
|
||||||
|
// warranty.
|
||||||
|
//
|
||||||
|
// THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||||
|
// SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
// FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||||
|
// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||||
|
// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||||
|
// THIS SOFTWARE.
|
||||||
|
|
||||||
|
package wlr_layer_shell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
xdg_shell "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/stable/xdg-shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwlrLayerShellV1InterfaceName = "zwlr_layer_shell_v1"
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1 : create surfaces that are layers of the desktop
|
||||||
|
//
|
||||||
|
// Clients can use this interface to assign the surface_layer role to
|
||||||
|
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||||
|
// rendered with a defined z-depth respective to each other. They may also be
|
||||||
|
// anchored to the edges and corners of a screen and specify input handling
|
||||||
|
// semantics. This interface should be suitable for the implementation of
|
||||||
|
// many desktop shell components, and a broad number of other applications
|
||||||
|
// that interact with the desktop.
|
||||||
|
type ZwlrLayerShellV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrLayerShellV1 : create surfaces that are layers of the desktop
|
||||||
|
//
|
||||||
|
// Clients can use this interface to assign the surface_layer role to
|
||||||
|
// wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||||
|
// rendered with a defined z-depth respective to each other. They may also be
|
||||||
|
// anchored to the edges and corners of a screen and specify input handling
|
||||||
|
// semantics. This interface should be suitable for the implementation of
|
||||||
|
// many desktop shell components, and a broad number of other applications
|
||||||
|
// that interact with the desktop.
|
||||||
|
func NewZwlrLayerShellV1(ctx *client.Context) *ZwlrLayerShellV1 {
|
||||||
|
zwlrLayerShellV1 := &ZwlrLayerShellV1{}
|
||||||
|
ctx.Register(zwlrLayerShellV1)
|
||||||
|
return zwlrLayerShellV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLayerSurface : create a layer_surface from a surface
|
||||||
|
//
|
||||||
|
// Create a layer surface for an existing surface. This assigns the role of
|
||||||
|
// layer_surface, or raises a protocol error if another role is already
|
||||||
|
// assigned.
|
||||||
|
//
|
||||||
|
// Creating a layer surface from a wl_surface which has a buffer attached
|
||||||
|
// or committed is a client error, and any attempts by a client to attach
|
||||||
|
// or manipulate a buffer prior to the first layer_surface.configure call
|
||||||
|
// must also be treated as errors.
|
||||||
|
//
|
||||||
|
// After creating a layer_surface object and setting it up, the client
|
||||||
|
// must perform an initial commit without any buffer attached.
|
||||||
|
// The compositor will reply with a layer_surface.configure event.
|
||||||
|
// The client must acknowledge it and is then allowed to attach a buffer
|
||||||
|
// to map the surface.
|
||||||
|
//
|
||||||
|
// You may pass NULL for output to allow the compositor to decide which
|
||||||
|
// output to use. Generally this will be the one that the user most
|
||||||
|
// recently interacted with.
|
||||||
|
//
|
||||||
|
// Clients can specify a namespace that defines the purpose of the layer
|
||||||
|
// surface.
|
||||||
|
//
|
||||||
|
// layer: layer to add this surface to
|
||||||
|
// namespace: namespace for the layer surface
|
||||||
|
func (i *ZwlrLayerShellV1) GetLayerSurface(surface *client.Surface, output *client.Output, layer uint32, namespace string) (*ZwlrLayerSurfaceV1, error) {
|
||||||
|
id := NewZwlrLayerSurfaceV1(i.Context())
|
||||||
|
const opcode = 0
|
||||||
|
namespaceLen := client.PaddedLen(len(namespace) + 1)
|
||||||
|
_reqBufLen := 8 + 4 + 4 + 4 + 4 + (4 + namespaceLen)
|
||||||
|
_reqBuf := make([]byte, _reqBufLen)
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||||
|
l += 4
|
||||||
|
if output == nil {
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], 0)
|
||||||
|
l += 4
|
||||||
|
} else {
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||||
|
l += 4
|
||||||
|
}
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(layer))
|
||||||
|
l += 4
|
||||||
|
client.PutString(_reqBuf[l:l+(4+namespaceLen)], namespace)
|
||||||
|
l += (4 + namespaceLen)
|
||||||
|
err := i.Context().WriteMsg(_reqBuf, nil)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the layer_shell object
|
||||||
|
//
|
||||||
|
// This request indicates that the client will not use the layer_shell
|
||||||
|
// object any more. Objects that have been created through this instance
|
||||||
|
// are not affected.
|
||||||
|
func (i *ZwlrLayerShellV1) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerShellV1Error uint32
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1Error :
|
||||||
|
const (
|
||||||
|
// ZwlrLayerShellV1ErrorRole : wl_surface has another role
|
||||||
|
ZwlrLayerShellV1ErrorRole ZwlrLayerShellV1Error = 0
|
||||||
|
// ZwlrLayerShellV1ErrorInvalidLayer : layer value is invalid
|
||||||
|
ZwlrLayerShellV1ErrorInvalidLayer ZwlrLayerShellV1Error = 1
|
||||||
|
// ZwlrLayerShellV1ErrorAlreadyConstructed : wl_surface has a buffer attached or committed
|
||||||
|
ZwlrLayerShellV1ErrorAlreadyConstructed ZwlrLayerShellV1Error = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1ErrorRole:
|
||||||
|
return "role"
|
||||||
|
case ZwlrLayerShellV1ErrorInvalidLayer:
|
||||||
|
return "invalid_layer"
|
||||||
|
case ZwlrLayerShellV1ErrorAlreadyConstructed:
|
||||||
|
return "already_constructed"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1ErrorRole:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerShellV1ErrorInvalidLayer:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerShellV1ErrorAlreadyConstructed:
|
||||||
|
return "2"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerShellV1Layer uint32
|
||||||
|
|
||||||
|
// ZwlrLayerShellV1Layer : available layers for surfaces
|
||||||
|
//
|
||||||
|
// These values indicate which layers a surface can be rendered in. They
|
||||||
|
// are ordered by z depth, bottom-most first. Traditional shell surfaces
|
||||||
|
// will typically be rendered between the bottom and top layers.
|
||||||
|
// Fullscreen shell surfaces are typically rendered at the top layer.
|
||||||
|
// Multiple surfaces can share a single layer, and ordering within a
|
||||||
|
// single layer is undefined.
|
||||||
|
const (
|
||||||
|
ZwlrLayerShellV1LayerBackground ZwlrLayerShellV1Layer = 0
|
||||||
|
ZwlrLayerShellV1LayerBottom ZwlrLayerShellV1Layer = 1
|
||||||
|
ZwlrLayerShellV1LayerTop ZwlrLayerShellV1Layer = 2
|
||||||
|
ZwlrLayerShellV1LayerOverlay ZwlrLayerShellV1Layer = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Layer) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1LayerBackground:
|
||||||
|
return "background"
|
||||||
|
case ZwlrLayerShellV1LayerBottom:
|
||||||
|
return "bottom"
|
||||||
|
case ZwlrLayerShellV1LayerTop:
|
||||||
|
return "top"
|
||||||
|
case ZwlrLayerShellV1LayerOverlay:
|
||||||
|
return "overlay"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Layer) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerShellV1LayerBackground:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerShellV1LayerBottom:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerShellV1LayerTop:
|
||||||
|
return "2"
|
||||||
|
case ZwlrLayerShellV1LayerOverlay:
|
||||||
|
return "3"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerShellV1Layer) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwlrLayerSurfaceV1InterfaceName = "zwlr_layer_surface_v1"
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1 : layer metadata interface
|
||||||
|
//
|
||||||
|
// An interface that may be implemented by a wl_surface, for surfaces that
|
||||||
|
// are designed to be rendered as a layer of a stacked desktop-like
|
||||||
|
// environment.
|
||||||
|
//
|
||||||
|
// Layer surface state (layer, size, anchor, exclusive zone,
|
||||||
|
// margin, interactivity) is double-buffered, and will be applied at the
|
||||||
|
// time wl_surface.commit of the corresponding wl_surface is called.
|
||||||
|
//
|
||||||
|
// Attaching a null buffer to a layer surface unmaps it.
|
||||||
|
//
|
||||||
|
// Unmapping a layer_surface means that the surface cannot be shown by the
|
||||||
|
// compositor until it is explicitly mapped again. The layer_surface
|
||||||
|
// returns to the state it had right after layer_shell.get_layer_surface.
|
||||||
|
// The client can re-map the surface by performing a commit without any
|
||||||
|
// buffer attached, waiting for a configure event and handling it as usual.
|
||||||
|
type ZwlrLayerSurfaceV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
configureHandler ZwlrLayerSurfaceV1ConfigureHandlerFunc
|
||||||
|
closedHandler ZwlrLayerSurfaceV1ClosedHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrLayerSurfaceV1 : layer metadata interface
|
||||||
|
//
|
||||||
|
// An interface that may be implemented by a wl_surface, for surfaces that
|
||||||
|
// are designed to be rendered as a layer of a stacked desktop-like
|
||||||
|
// environment.
|
||||||
|
//
|
||||||
|
// Layer surface state (layer, size, anchor, exclusive zone,
|
||||||
|
// margin, interactivity) is double-buffered, and will be applied at the
|
||||||
|
// time wl_surface.commit of the corresponding wl_surface is called.
|
||||||
|
//
|
||||||
|
// Attaching a null buffer to a layer surface unmaps it.
|
||||||
|
//
|
||||||
|
// Unmapping a layer_surface means that the surface cannot be shown by the
|
||||||
|
// compositor until it is explicitly mapped again. The layer_surface
|
||||||
|
// returns to the state it had right after layer_shell.get_layer_surface.
|
||||||
|
// The client can re-map the surface by performing a commit without any
|
||||||
|
// buffer attached, waiting for a configure event and handling it as usual.
|
||||||
|
func NewZwlrLayerSurfaceV1(ctx *client.Context) *ZwlrLayerSurfaceV1 {
|
||||||
|
zwlrLayerSurfaceV1 := &ZwlrLayerSurfaceV1{}
|
||||||
|
ctx.Register(zwlrLayerSurfaceV1)
|
||||||
|
return zwlrLayerSurfaceV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize : sets the size of the surface
|
||||||
|
//
|
||||||
|
// Sets the size of the surface in surface-local coordinates. The
|
||||||
|
// compositor will display the surface centered with respect to its
|
||||||
|
// anchors.
|
||||||
|
//
|
||||||
|
// If you pass 0 for either value, the compositor will assign it and
|
||||||
|
// inform you of the assignment in the configure event. You must set your
|
||||||
|
// anchor to opposite edges in the dimensions you omit; not doing so is a
|
||||||
|
// protocol error. Both values are 0 by default.
|
||||||
|
//
|
||||||
|
// Size is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetSize(width, height uint32) error {
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(width))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAnchor : configures the anchor point of the surface
|
||||||
|
//
|
||||||
|
// Requests that the compositor anchor the surface to the specified edges
|
||||||
|
// and corners. If two orthogonal edges are specified (e.g. 'top' and
|
||||||
|
// 'left'), then the anchor point will be the intersection of the edges
|
||||||
|
// (e.g. the top left corner of the output); otherwise the anchor point
|
||||||
|
// will be centered on that edge, or in the center if none is specified.
|
||||||
|
//
|
||||||
|
// Anchor is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetAnchor(anchor uint32) error {
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(anchor))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExclusiveZone : configures the exclusive geometry of this surface
|
||||||
|
//
|
||||||
|
// Requests that the compositor avoids occluding an area with other
|
||||||
|
// surfaces. The compositor's use of this information is
|
||||||
|
// implementation-dependent - do not assume that this region will not
|
||||||
|
// actually be occluded.
|
||||||
|
//
|
||||||
|
// A positive value is only meaningful if the surface is anchored to one
|
||||||
|
// edge or an edge and both perpendicular edges. If the surface is not
|
||||||
|
// anchored, anchored to only two perpendicular edges (a corner), anchored
|
||||||
|
// to only two parallel edges or anchored to all edges, a positive value
|
||||||
|
// will be treated the same as zero.
|
||||||
|
//
|
||||||
|
// A positive zone is the distance from the edge in surface-local
|
||||||
|
// coordinates to consider exclusive.
|
||||||
|
//
|
||||||
|
// Surfaces that do not wish to have an exclusive zone may instead specify
|
||||||
|
// how they should interact with surfaces that do. If set to zero, the
|
||||||
|
// surface indicates that it would like to be moved to avoid occluding
|
||||||
|
// surfaces with a positive exclusive zone. If set to -1, the surface
|
||||||
|
// indicates that it would not like to be moved to accommodate for other
|
||||||
|
// surfaces, and the compositor should extend it all the way to the edges
|
||||||
|
// it is anchored to.
|
||||||
|
//
|
||||||
|
// For example, a panel might set its exclusive zone to 10, so that
|
||||||
|
// maximized shell surfaces are not shown on top of it. A notification
|
||||||
|
// might set its exclusive zone to 0, so that it is moved to avoid
|
||||||
|
// occluding the panel, but shell surfaces are shown underneath it. A
|
||||||
|
// wallpaper or lock screen might set their exclusive zone to -1, so that
|
||||||
|
// they stretch below or over the panel.
|
||||||
|
//
|
||||||
|
// The default value is 0.
|
||||||
|
//
|
||||||
|
// Exclusive zone is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetExclusiveZone(zone int32) error {
|
||||||
|
const opcode = 2
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(zone))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMargin : sets a margin from the anchor point
|
||||||
|
//
|
||||||
|
// Requests that the surface be placed some distance away from the anchor
|
||||||
|
// point on the output, in surface-local coordinates. Setting this value
|
||||||
|
// for edges you are not anchored to has no effect.
|
||||||
|
//
|
||||||
|
// The exclusive zone includes the margin.
|
||||||
|
//
|
||||||
|
// Margin is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetMargin(top, right, bottom, left int32) error {
|
||||||
|
const opcode = 3
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(top))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(right))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(bottom))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(left))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyboardInteractivity : requests keyboard events
|
||||||
|
//
|
||||||
|
// Set how keyboard events are delivered to this surface. By default,
|
||||||
|
// layer shell surfaces do not receive keyboard events; this request can
|
||||||
|
// be used to change this.
|
||||||
|
//
|
||||||
|
// This setting is inherited by child surfaces set by the get_popup
|
||||||
|
// request.
|
||||||
|
//
|
||||||
|
// Layer surfaces receive pointer, touch, and tablet events normally. If
|
||||||
|
// you do not want to receive them, set the input region on your surface
|
||||||
|
// to an empty region.
|
||||||
|
//
|
||||||
|
// Keyboard interactivity is double-buffered, see wl_surface.commit.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetKeyboardInteractivity(keyboardInteractivity uint32) error {
|
||||||
|
const opcode = 4
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(keyboardInteractivity))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPopup : assign this layer_surface as an xdg_popup parent
|
||||||
|
//
|
||||||
|
// This assigns an xdg_popup's parent to this layer_surface. This popup
|
||||||
|
// should have been created via xdg_surface::get_popup with the parent set
|
||||||
|
// to NULL, and this request must be invoked before committing the popup's
|
||||||
|
// initial state.
|
||||||
|
//
|
||||||
|
// See the documentation of xdg_popup for more details about what an
|
||||||
|
// xdg_popup is and how it is used.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) GetPopup(popup *xdg_shell.Popup) error {
|
||||||
|
const opcode = 5
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], popup.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AckConfigure : ack a configure event
|
||||||
|
//
|
||||||
|
// When a configure event is received, if a client commits the
|
||||||
|
// surface in response to the configure event, then the client
|
||||||
|
// must make an ack_configure request sometime before the commit
|
||||||
|
// request, passing along the serial of the configure event.
|
||||||
|
//
|
||||||
|
// If the client receives multiple configure events before it
|
||||||
|
// can respond to one, it only has to ack the last configure event.
|
||||||
|
//
|
||||||
|
// A client is not required to commit immediately after sending
|
||||||
|
// an ack_configure request - it may even ack_configure several times
|
||||||
|
// before its next surface commit.
|
||||||
|
//
|
||||||
|
// A client may send multiple ack_configure requests before committing, but
|
||||||
|
// only the last request sent before a commit indicates which configure
|
||||||
|
// event the client really is responding to.
|
||||||
|
//
|
||||||
|
// serial: the serial from the configure event
|
||||||
|
func (i *ZwlrLayerSurfaceV1) AckConfigure(serial uint32) error {
|
||||||
|
const opcode = 6
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(serial))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the layer_surface
|
||||||
|
//
|
||||||
|
// This request destroys the layer surface.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 7
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLayer : change the layer of the surface
|
||||||
|
//
|
||||||
|
// Change the layer that the surface is rendered on.
|
||||||
|
//
|
||||||
|
// Layer is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// layer: layer to move this surface to
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetLayer(layer uint32) error {
|
||||||
|
const opcode = 8
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(layer))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExclusiveEdge : set the edge the exclusive zone will be applied to
|
||||||
|
//
|
||||||
|
// Requests an edge for the exclusive zone to apply. The exclusive
|
||||||
|
// edge will be automatically deduced from anchor points when possible,
|
||||||
|
// but when the surface is anchored to a corner, it will be necessary
|
||||||
|
// to set it explicitly to disambiguate, as it is not possible to deduce
|
||||||
|
// which one of the two corner edges should be used.
|
||||||
|
//
|
||||||
|
// The edge must be one the surface is anchored to, otherwise the
|
||||||
|
// invalid_exclusive_edge protocol error will be raised.
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetExclusiveEdge(edge uint32) error {
|
||||||
|
const opcode = 9
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(edge))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerSurfaceV1KeyboardInteractivity uint32
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1KeyboardInteractivity : types of keyboard interaction possible for a layer shell surface
|
||||||
|
//
|
||||||
|
// Types of keyboard interaction possible for layer shell surfaces. The
|
||||||
|
// rationale for this is twofold: (1) some applications are not interested
|
||||||
|
// in keyboard events and not allowing them to be focused can improve the
|
||||||
|
// desktop experience; (2) some applications will want to take exclusive
|
||||||
|
// keyboard focus.
|
||||||
|
const (
|
||||||
|
ZwlrLayerSurfaceV1KeyboardInteractivityNone ZwlrLayerSurfaceV1KeyboardInteractivity = 0
|
||||||
|
ZwlrLayerSurfaceV1KeyboardInteractivityExclusive ZwlrLayerSurfaceV1KeyboardInteractivity = 1
|
||||||
|
ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand ZwlrLayerSurfaceV1KeyboardInteractivity = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
|
||||||
|
return "none"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
|
||||||
|
return "exclusive"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
|
||||||
|
return "on_demand"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityNone:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityExclusive:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerSurfaceV1KeyboardInteractivityOnDemand:
|
||||||
|
return "2"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1KeyboardInteractivity) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerSurfaceV1Error uint32
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1Error :
|
||||||
|
const (
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidSurfaceState : provided surface state is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidSurfaceState ZwlrLayerSurfaceV1Error = 0
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidSize : size is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidSize ZwlrLayerSurfaceV1Error = 1
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidAnchor : anchor bitfield is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidAnchor ZwlrLayerSurfaceV1Error = 2
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity : keyboard interactivity is invalid
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity ZwlrLayerSurfaceV1Error = 3
|
||||||
|
// ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge : exclusive edge is invalid given the surface anchors
|
||||||
|
ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge ZwlrLayerSurfaceV1Error = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
|
||||||
|
return "invalid_surface_state"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSize:
|
||||||
|
return "invalid_size"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
|
||||||
|
return "invalid_anchor"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
|
||||||
|
return "invalid_keyboard_interactivity"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
|
||||||
|
return "invalid_exclusive_edge"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSurfaceState:
|
||||||
|
return "0"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidSize:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidAnchor:
|
||||||
|
return "2"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity:
|
||||||
|
return "3"
|
||||||
|
case ZwlrLayerSurfaceV1ErrorInvalidExclusiveEdge:
|
||||||
|
return "4"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrLayerSurfaceV1Anchor uint32
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1Anchor :
|
||||||
|
const (
|
||||||
|
// ZwlrLayerSurfaceV1AnchorTop : the top edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorTop ZwlrLayerSurfaceV1Anchor = 1
|
||||||
|
// ZwlrLayerSurfaceV1AnchorBottom : the bottom edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorBottom ZwlrLayerSurfaceV1Anchor = 2
|
||||||
|
// ZwlrLayerSurfaceV1AnchorLeft : the left edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorLeft ZwlrLayerSurfaceV1Anchor = 4
|
||||||
|
// ZwlrLayerSurfaceV1AnchorRight : the right edge of the anchor rectangle
|
||||||
|
ZwlrLayerSurfaceV1AnchorRight ZwlrLayerSurfaceV1Anchor = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Anchor) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1AnchorTop:
|
||||||
|
return "top"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorBottom:
|
||||||
|
return "bottom"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorLeft:
|
||||||
|
return "left"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorRight:
|
||||||
|
return "right"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Anchor) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrLayerSurfaceV1AnchorTop:
|
||||||
|
return "1"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorBottom:
|
||||||
|
return "2"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorLeft:
|
||||||
|
return "4"
|
||||||
|
case ZwlrLayerSurfaceV1AnchorRight:
|
||||||
|
return "8"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrLayerSurfaceV1Anchor) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1ConfigureEvent : suggest a surface change
|
||||||
|
//
|
||||||
|
// The configure event asks the client to resize its surface.
|
||||||
|
//
|
||||||
|
// Clients should arrange their surface for the new states, and then send
|
||||||
|
// an ack_configure request with the serial sent in this configure event at
|
||||||
|
// some point before committing the new surface.
|
||||||
|
//
|
||||||
|
// The client is free to dismiss all but the last configure event it
|
||||||
|
// received.
|
||||||
|
//
|
||||||
|
// The width and height arguments specify the size of the window in
|
||||||
|
// surface-local coordinates.
|
||||||
|
//
|
||||||
|
// The size is a hint, in the sense that the client is free to ignore it if
|
||||||
|
// it doesn't resize, pick a smaller size (to satisfy aspect ratio or
|
||||||
|
// resize in steps of NxM pixels). If the client picks a smaller size and
|
||||||
|
// is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
|
||||||
|
// surface will be centered on this axis.
|
||||||
|
//
|
||||||
|
// If the width or height arguments are zero, it means the client should
|
||||||
|
// decide its own window dimension.
|
||||||
|
type ZwlrLayerSurfaceV1ConfigureEvent struct {
|
||||||
|
Serial uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
}
|
||||||
|
type ZwlrLayerSurfaceV1ConfigureHandlerFunc func(ZwlrLayerSurfaceV1ConfigureEvent)
|
||||||
|
|
||||||
|
// SetConfigureHandler : sets handler for ZwlrLayerSurfaceV1ConfigureEvent
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetConfigureHandler(f ZwlrLayerSurfaceV1ConfigureHandlerFunc) {
|
||||||
|
i.configureHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrLayerSurfaceV1ClosedEvent : surface should be closed
|
||||||
|
//
|
||||||
|
// The closed event is sent by the compositor when the surface will no
|
||||||
|
// longer be shown. The output may have been destroyed or the user may
|
||||||
|
// have asked for it to be removed. Further changes to the surface will be
|
||||||
|
// ignored. The client should destroy the resource after receiving this
|
||||||
|
// event, and create a new surface if they so choose.
|
||||||
|
type ZwlrLayerSurfaceV1ClosedEvent struct{}
|
||||||
|
type ZwlrLayerSurfaceV1ClosedHandlerFunc func(ZwlrLayerSurfaceV1ClosedEvent)
|
||||||
|
|
||||||
|
// SetClosedHandler : sets handler for ZwlrLayerSurfaceV1ClosedEvent
|
||||||
|
func (i *ZwlrLayerSurfaceV1) SetClosedHandler(f ZwlrLayerSurfaceV1ClosedHandlerFunc) {
|
||||||
|
i.closedHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ZwlrLayerSurfaceV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||||
|
switch opcode {
|
||||||
|
case 0:
|
||||||
|
if i.configureHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrLayerSurfaceV1ConfigureEvent
|
||||||
|
l := 0
|
||||||
|
e.Serial = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.configureHandler(e)
|
||||||
|
case 1:
|
||||||
|
if i.closedHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrLayerSurfaceV1ClosedEvent
|
||||||
|
|
||||||
|
i.closedHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -172,7 +172,7 @@ func (i *ZwlrOutputManagerV1) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ZwlrOutputManagerV1) Destroy() error {
|
func (i *ZwlrOutputManagerV1) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ func NewZwlrOutputHeadV1(ctx *client.Context) *ZwlrOutputHeadV1 {
|
|||||||
// This request indicates that the client will no longer use this head
|
// This request indicates that the client will no longer use this head
|
||||||
// object.
|
// object.
|
||||||
func (i *ZwlrOutputHeadV1) Release() error {
|
func (i *ZwlrOutputHeadV1) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -879,7 +879,7 @@ func NewZwlrOutputModeV1(ctx *client.Context) *ZwlrOutputModeV1 {
|
|||||||
// This request indicates that the client will no longer use this mode
|
// This request indicates that the client will no longer use this mode
|
||||||
// object.
|
// object.
|
||||||
func (i *ZwlrOutputModeV1) Release() error {
|
func (i *ZwlrOutputModeV1) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -1132,7 +1132,7 @@ func (i *ZwlrOutputConfigurationV1) Test() error {
|
|||||||
// This request also destroys wlr_output_configuration_head objects created
|
// This request also destroys wlr_output_configuration_head objects created
|
||||||
// via this object.
|
// via this object.
|
||||||
func (i *ZwlrOutputConfigurationV1) Destroy() error {
|
func (i *ZwlrOutputConfigurationV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 4
|
const opcode = 4
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -1415,7 +1415,7 @@ func (i *ZwlrOutputConfigurationHeadV1) SetAdaptiveSync(state uint32) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ZwlrOutputConfigurationHeadV1) Destroy() error {
|
func (i *ZwlrOutputConfigurationHeadV1) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (i *ZwlrOutputPowerManagerV1) GetOutputPower(output *client.Output) (*ZwlrO
|
|||||||
// All objects created by the manager will still remain valid, until their
|
// All objects created by the manager will still remain valid, until their
|
||||||
// appropriate destroy request has been called.
|
// appropriate destroy request has been called.
|
||||||
func (i *ZwlrOutputPowerManagerV1) Destroy() error {
|
func (i *ZwlrOutputPowerManagerV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -143,7 +143,7 @@ func (i *ZwlrOutputPowerV1) SetMode(mode uint32) error {
|
|||||||
//
|
//
|
||||||
// Destroys the output power management mode control object.
|
// Destroys the output power management mode control object.
|
||||||
func (i *ZwlrOutputPowerV1) Destroy() error {
|
func (i *ZwlrOutputPowerV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
532
core/internal/proto/wlr_screencopy/screencopy.go
Normal file
532
core/internal/proto/wlr_screencopy/screencopy.go
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : internal/proto/xml/wlr-screencopy-unstable-v1.xml
|
||||||
|
//
|
||||||
|
// wlr_screencopy_unstable_v1 Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2018 Simon Ser
|
||||||
|
// Copyright © 2019 Andri Yngvason
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
// copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice (including the next
|
||||||
|
// paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
// Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package wlr_screencopy
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
|
||||||
|
// ZwlrScreencopyManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwlrScreencopyManagerV1InterfaceName = "zwlr_screencopy_manager_v1"
|
||||||
|
|
||||||
|
// ZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
|
||||||
|
//
|
||||||
|
// This object is a manager which offers requests to start capturing from a
|
||||||
|
// source.
|
||||||
|
type ZwlrScreencopyManagerV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrScreencopyManagerV1 : manager to inform clients and begin capturing
|
||||||
|
//
|
||||||
|
// This object is a manager which offers requests to start capturing from a
|
||||||
|
// source.
|
||||||
|
func NewZwlrScreencopyManagerV1(ctx *client.Context) *ZwlrScreencopyManagerV1 {
|
||||||
|
zwlrScreencopyManagerV1 := &ZwlrScreencopyManagerV1{}
|
||||||
|
ctx.Register(zwlrScreencopyManagerV1)
|
||||||
|
return zwlrScreencopyManagerV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureOutput : capture an output
|
||||||
|
//
|
||||||
|
// Capture the next frame of an entire output.
|
||||||
|
//
|
||||||
|
// overlayCursor: composite cursor onto the frame
|
||||||
|
func (i *ZwlrScreencopyManagerV1) CaptureOutput(overlayCursor int32, output *client.Output) (*ZwlrScreencopyFrameV1, error) {
|
||||||
|
frame := NewZwlrScreencopyFrameV1(i.Context())
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], frame.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return frame, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureOutputRegion : capture an output's region
|
||||||
|
//
|
||||||
|
// Capture the next frame of an output's region.
|
||||||
|
//
|
||||||
|
// The region is given in output logical coordinates, see
|
||||||
|
// xdg_output.logical_size. The region will be clipped to the output's
|
||||||
|
// extents.
|
||||||
|
//
|
||||||
|
// overlayCursor: composite cursor onto the frame
|
||||||
|
func (i *ZwlrScreencopyManagerV1) CaptureOutputRegion(overlayCursor int32, output *client.Output, x, y, width, height int32) (*ZwlrScreencopyFrameV1, error) {
|
||||||
|
frame := NewZwlrScreencopyFrameV1(i.Context())
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4 + 4 + 4 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], frame.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(x))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(y))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(width))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return frame, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : destroy the manager
|
||||||
|
//
|
||||||
|
// All objects created by the manager will still remain valid, until their
|
||||||
|
// appropriate destroy request has been called.
|
||||||
|
func (i *ZwlrScreencopyManagerV1) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 2
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const ZwlrScreencopyFrameV1InterfaceName = "zwlr_screencopy_frame_v1"
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1 : a frame ready for copy
|
||||||
|
//
|
||||||
|
// This object represents a single frame.
|
||||||
|
//
|
||||||
|
// When created, a series of buffer events will be sent, each representing a
|
||||||
|
// supported buffer type. The "buffer_done" event is sent afterwards to
|
||||||
|
// indicate that all supported buffer types have been enumerated. The client
|
||||||
|
// will then be able to send a "copy" request. If the capture is successful,
|
||||||
|
// the compositor will send a "flags" event followed by a "ready" event.
|
||||||
|
//
|
||||||
|
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||||
|
// the "buffer" event is guaranteed to be sent.
|
||||||
|
//
|
||||||
|
// If the capture failed, the "failed" event is sent. This can happen anytime
|
||||||
|
// before the "ready" event.
|
||||||
|
//
|
||||||
|
// Once either a "ready" or a "failed" event is received, the client should
|
||||||
|
// destroy the frame.
|
||||||
|
type ZwlrScreencopyFrameV1 struct {
|
||||||
|
client.BaseProxy
|
||||||
|
bufferHandler ZwlrScreencopyFrameV1BufferHandlerFunc
|
||||||
|
flagsHandler ZwlrScreencopyFrameV1FlagsHandlerFunc
|
||||||
|
readyHandler ZwlrScreencopyFrameV1ReadyHandlerFunc
|
||||||
|
failedHandler ZwlrScreencopyFrameV1FailedHandlerFunc
|
||||||
|
damageHandler ZwlrScreencopyFrameV1DamageHandlerFunc
|
||||||
|
linuxDmabufHandler ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc
|
||||||
|
bufferDoneHandler ZwlrScreencopyFrameV1BufferDoneHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewZwlrScreencopyFrameV1 : a frame ready for copy
|
||||||
|
//
|
||||||
|
// This object represents a single frame.
|
||||||
|
//
|
||||||
|
// When created, a series of buffer events will be sent, each representing a
|
||||||
|
// supported buffer type. The "buffer_done" event is sent afterwards to
|
||||||
|
// indicate that all supported buffer types have been enumerated. The client
|
||||||
|
// will then be able to send a "copy" request. If the capture is successful,
|
||||||
|
// the compositor will send a "flags" event followed by a "ready" event.
|
||||||
|
//
|
||||||
|
// For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||||
|
// the "buffer" event is guaranteed to be sent.
|
||||||
|
//
|
||||||
|
// If the capture failed, the "failed" event is sent. This can happen anytime
|
||||||
|
// before the "ready" event.
|
||||||
|
//
|
||||||
|
// Once either a "ready" or a "failed" event is received, the client should
|
||||||
|
// destroy the frame.
|
||||||
|
func NewZwlrScreencopyFrameV1(ctx *client.Context) *ZwlrScreencopyFrameV1 {
|
||||||
|
zwlrScreencopyFrameV1 := &ZwlrScreencopyFrameV1{}
|
||||||
|
ctx.Register(zwlrScreencopyFrameV1)
|
||||||
|
return zwlrScreencopyFrameV1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy : copy the frame
|
||||||
|
//
|
||||||
|
// Copy the frame to the supplied buffer. The buffer must have the
|
||||||
|
// correct size, see zwlr_screencopy_frame_v1.buffer and
|
||||||
|
// zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
|
||||||
|
// supported format.
|
||||||
|
//
|
||||||
|
// If the frame is successfully copied, "flags" and "ready" events are
|
||||||
|
// sent. Otherwise, a "failed" event is sent.
|
||||||
|
func (i *ZwlrScreencopyFrameV1) Copy(buffer *client.Buffer) error {
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], buffer.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : delete this object, used or not
|
||||||
|
//
|
||||||
|
// Destroys the frame. This request can be sent at any time by the client.
|
||||||
|
func (i *ZwlrScreencopyFrameV1) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyWithDamage : copy the frame when it's damaged
|
||||||
|
//
|
||||||
|
// Same as copy, except it waits until there is damage to copy.
|
||||||
|
func (i *ZwlrScreencopyFrameV1) CopyWithDamage(buffer *client.Buffer) error {
|
||||||
|
const opcode = 2
|
||||||
|
const _reqBufLen = 8 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], buffer.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrScreencopyFrameV1Error uint32
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1Error :
|
||||||
|
const (
|
||||||
|
// ZwlrScreencopyFrameV1ErrorAlreadyUsed : the object has already been used to copy a wl_buffer
|
||||||
|
ZwlrScreencopyFrameV1ErrorAlreadyUsed ZwlrScreencopyFrameV1Error = 0
|
||||||
|
// ZwlrScreencopyFrameV1ErrorInvalidBuffer : buffer attributes are invalid
|
||||||
|
ZwlrScreencopyFrameV1ErrorInvalidBuffer ZwlrScreencopyFrameV1Error = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Error) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
|
||||||
|
return "already_used"
|
||||||
|
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
|
||||||
|
return "invalid_buffer"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Error) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1ErrorAlreadyUsed:
|
||||||
|
return "0"
|
||||||
|
case ZwlrScreencopyFrameV1ErrorInvalidBuffer:
|
||||||
|
return "1"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Error) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZwlrScreencopyFrameV1Flags uint32
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1Flags :
|
||||||
|
const (
|
||||||
|
// ZwlrScreencopyFrameV1FlagsYInvert : contents are y-inverted
|
||||||
|
ZwlrScreencopyFrameV1FlagsYInvert ZwlrScreencopyFrameV1Flags = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Flags) Name() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1FlagsYInvert:
|
||||||
|
return "y_invert"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Flags) Value() string {
|
||||||
|
switch e {
|
||||||
|
case ZwlrScreencopyFrameV1FlagsYInvert:
|
||||||
|
return "1"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ZwlrScreencopyFrameV1Flags) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1BufferEvent : wl_shm buffer information
|
||||||
|
//
|
||||||
|
// Provides information about wl_shm buffer parameters that need to be
|
||||||
|
// used for this frame. This event is sent once after the frame is created
|
||||||
|
// if wl_shm buffers are supported.
|
||||||
|
type ZwlrScreencopyFrameV1BufferEvent struct {
|
||||||
|
Format uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
Stride uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1BufferHandlerFunc func(ZwlrScreencopyFrameV1BufferEvent)
|
||||||
|
|
||||||
|
// SetBufferHandler : sets handler for ZwlrScreencopyFrameV1BufferEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetBufferHandler(f ZwlrScreencopyFrameV1BufferHandlerFunc) {
|
||||||
|
i.bufferHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1FlagsEvent : frame flags
|
||||||
|
//
|
||||||
|
// Provides flags about the frame. This event is sent once before the
|
||||||
|
// "ready" event.
|
||||||
|
type ZwlrScreencopyFrameV1FlagsEvent struct {
|
||||||
|
Flags uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1FlagsHandlerFunc func(ZwlrScreencopyFrameV1FlagsEvent)
|
||||||
|
|
||||||
|
// SetFlagsHandler : sets handler for ZwlrScreencopyFrameV1FlagsEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetFlagsHandler(f ZwlrScreencopyFrameV1FlagsHandlerFunc) {
|
||||||
|
i.flagsHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1ReadyEvent : indicates frame is available for reading
|
||||||
|
//
|
||||||
|
// Called as soon as the frame is copied, indicating it is available
|
||||||
|
// for reading. This event includes the time at which the presentation took place.
|
||||||
|
//
|
||||||
|
// The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
|
||||||
|
// each component being an unsigned 32-bit value. Whole seconds are in
|
||||||
|
// tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
|
||||||
|
// and the additional fractional part in tv_nsec as nanoseconds. Hence,
|
||||||
|
// for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
|
||||||
|
// may have an arbitrary offset at start.
|
||||||
|
//
|
||||||
|
// After receiving this event, the client should destroy the object.
|
||||||
|
type ZwlrScreencopyFrameV1ReadyEvent struct {
|
||||||
|
TvSecHi uint32
|
||||||
|
TvSecLo uint32
|
||||||
|
TvNsec uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1ReadyHandlerFunc func(ZwlrScreencopyFrameV1ReadyEvent)
|
||||||
|
|
||||||
|
// SetReadyHandler : sets handler for ZwlrScreencopyFrameV1ReadyEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetReadyHandler(f ZwlrScreencopyFrameV1ReadyHandlerFunc) {
|
||||||
|
i.readyHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1FailedEvent : frame copy failed
|
||||||
|
//
|
||||||
|
// This event indicates that the attempted frame copy has failed.
|
||||||
|
//
|
||||||
|
// After receiving this event, the client should destroy the object.
|
||||||
|
type ZwlrScreencopyFrameV1FailedEvent struct{}
|
||||||
|
type ZwlrScreencopyFrameV1FailedHandlerFunc func(ZwlrScreencopyFrameV1FailedEvent)
|
||||||
|
|
||||||
|
// SetFailedHandler : sets handler for ZwlrScreencopyFrameV1FailedEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetFailedHandler(f ZwlrScreencopyFrameV1FailedHandlerFunc) {
|
||||||
|
i.failedHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1DamageEvent : carries the coordinates of the damaged region
|
||||||
|
//
|
||||||
|
// This event is sent right before the ready event when copy_with_damage is
|
||||||
|
// requested. It may be generated multiple times for each copy_with_damage
|
||||||
|
// request.
|
||||||
|
//
|
||||||
|
// The arguments describe a box around an area that has changed since the
|
||||||
|
// last copy request that was derived from the current screencopy manager
|
||||||
|
// instance.
|
||||||
|
//
|
||||||
|
// The union of all regions received between the call to copy_with_damage
|
||||||
|
// and a ready event is the total damage since the prior ready event.
|
||||||
|
type ZwlrScreencopyFrameV1DamageEvent struct {
|
||||||
|
X uint32
|
||||||
|
Y uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1DamageHandlerFunc func(ZwlrScreencopyFrameV1DamageEvent)
|
||||||
|
|
||||||
|
// SetDamageHandler : sets handler for ZwlrScreencopyFrameV1DamageEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetDamageHandler(f ZwlrScreencopyFrameV1DamageHandlerFunc) {
|
||||||
|
i.damageHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1LinuxDmabufEvent : linux-dmabuf buffer information
|
||||||
|
//
|
||||||
|
// Provides information about linux-dmabuf buffer parameters that need to
|
||||||
|
// be used for this frame. This event is sent once after the frame is
|
||||||
|
// created if linux-dmabuf buffers are supported.
|
||||||
|
type ZwlrScreencopyFrameV1LinuxDmabufEvent struct {
|
||||||
|
Format uint32
|
||||||
|
Width uint32
|
||||||
|
Height uint32
|
||||||
|
}
|
||||||
|
type ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc func(ZwlrScreencopyFrameV1LinuxDmabufEvent)
|
||||||
|
|
||||||
|
// SetLinuxDmabufHandler : sets handler for ZwlrScreencopyFrameV1LinuxDmabufEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetLinuxDmabufHandler(f ZwlrScreencopyFrameV1LinuxDmabufHandlerFunc) {
|
||||||
|
i.linuxDmabufHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZwlrScreencopyFrameV1BufferDoneEvent : all buffer types reported
|
||||||
|
//
|
||||||
|
// This event is sent once after all buffer events have been sent.
|
||||||
|
//
|
||||||
|
// The client should proceed to create a buffer of one of the supported
|
||||||
|
// types, and send a "copy" request.
|
||||||
|
type ZwlrScreencopyFrameV1BufferDoneEvent struct{}
|
||||||
|
type ZwlrScreencopyFrameV1BufferDoneHandlerFunc func(ZwlrScreencopyFrameV1BufferDoneEvent)
|
||||||
|
|
||||||
|
// SetBufferDoneHandler : sets handler for ZwlrScreencopyFrameV1BufferDoneEvent
|
||||||
|
func (i *ZwlrScreencopyFrameV1) SetBufferDoneHandler(f ZwlrScreencopyFrameV1BufferDoneHandlerFunc) {
|
||||||
|
i.bufferDoneHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ZwlrScreencopyFrameV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||||
|
switch opcode {
|
||||||
|
case 0:
|
||||||
|
if i.bufferHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1BufferEvent
|
||||||
|
l := 0
|
||||||
|
e.Format = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Stride = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.bufferHandler(e)
|
||||||
|
case 1:
|
||||||
|
if i.flagsHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1FlagsEvent
|
||||||
|
l := 0
|
||||||
|
e.Flags = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.flagsHandler(e)
|
||||||
|
case 2:
|
||||||
|
if i.readyHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1ReadyEvent
|
||||||
|
l := 0
|
||||||
|
e.TvSecHi = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.TvSecLo = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.TvNsec = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.readyHandler(e)
|
||||||
|
case 3:
|
||||||
|
if i.failedHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1FailedEvent
|
||||||
|
|
||||||
|
i.failedHandler(e)
|
||||||
|
case 4:
|
||||||
|
if i.damageHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1DamageEvent
|
||||||
|
l := 0
|
||||||
|
e.X = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Y = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.damageHandler(e)
|
||||||
|
case 5:
|
||||||
|
if i.linuxDmabufHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1LinuxDmabufEvent
|
||||||
|
l := 0
|
||||||
|
e.Format = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Width = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
e.Height = client.Uint32(data[l : l+4])
|
||||||
|
l += 4
|
||||||
|
|
||||||
|
i.linuxDmabufHandler(e)
|
||||||
|
case 6:
|
||||||
|
if i.bufferDoneHandler == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var e ZwlrScreencopyFrameV1BufferDoneEvent
|
||||||
|
|
||||||
|
i.bufferDoneHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
399
core/internal/proto/wp_viewporter/viewporter.go
Normal file
399
core/internal/proto/wp_viewporter/viewporter.go
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
// Generated by go-wayland-scanner
|
||||||
|
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||||
|
// XML file : /tmp/viewporter.xml
|
||||||
|
//
|
||||||
|
// viewporter Protocol Copyright:
|
||||||
|
//
|
||||||
|
// Copyright © 2013-2016 Collabora, Ltd.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
// copy of this software and associated documentation files (the "Software"),
|
||||||
|
// to deal in the Software without restriction, including without limitation
|
||||||
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
// and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
// Software is furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice (including the next
|
||||||
|
// paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
// Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
package wp_viewporter
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
|
||||||
|
// WpViewporterInterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const WpViewporterInterfaceName = "wp_viewporter"
|
||||||
|
|
||||||
|
// WpViewporter : surface cropping and scaling
|
||||||
|
//
|
||||||
|
// The global interface exposing surface cropping and scaling
|
||||||
|
// capabilities is used to instantiate an interface extension for a
|
||||||
|
// wl_surface object. This extended interface will then allow
|
||||||
|
// cropping and scaling the surface contents, effectively
|
||||||
|
// disconnecting the direct relationship between the buffer and the
|
||||||
|
// surface size.
|
||||||
|
type WpViewporter struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWpViewporter : surface cropping and scaling
|
||||||
|
//
|
||||||
|
// The global interface exposing surface cropping and scaling
|
||||||
|
// capabilities is used to instantiate an interface extension for a
|
||||||
|
// wl_surface object. This extended interface will then allow
|
||||||
|
// cropping and scaling the surface contents, effectively
|
||||||
|
// disconnecting the direct relationship between the buffer and the
|
||||||
|
// surface size.
|
||||||
|
func NewWpViewporter(ctx *client.Context) *WpViewporter {
|
||||||
|
wpViewporter := &WpViewporter{}
|
||||||
|
ctx.Register(wpViewporter)
|
||||||
|
return wpViewporter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : unbind from the cropping and scaling interface
|
||||||
|
//
|
||||||
|
// Informs the server that the client will not be using this
|
||||||
|
// protocol object anymore. This does not affect any other objects,
|
||||||
|
// wp_viewport objects included.
|
||||||
|
func (i *WpViewporter) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewport : extend surface interface for crop and scale
|
||||||
|
//
|
||||||
|
// Instantiate an interface extension for the given wl_surface to
|
||||||
|
// crop and scale its content. If the given wl_surface already has
|
||||||
|
// a wp_viewport object associated, the viewport_exists
|
||||||
|
// protocol error is raised.
|
||||||
|
//
|
||||||
|
// surface: the surface
|
||||||
|
func (i *WpViewporter) GetViewport(surface *client.Surface) (*WpViewport, error) {
|
||||||
|
id := NewWpViewport(i.Context())
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], surface.ID())
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type WpViewporterError uint32
|
||||||
|
|
||||||
|
// WpViewporterError :
|
||||||
|
const (
|
||||||
|
// WpViewporterErrorViewportExists : the surface already has a viewport object associated
|
||||||
|
WpViewporterErrorViewportExists WpViewporterError = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e WpViewporterError) Name() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewporterErrorViewportExists:
|
||||||
|
return "viewport_exists"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewporterError) Value() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewporterErrorViewportExists:
|
||||||
|
return "0"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewporterError) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WpViewportInterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||||
|
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||||
|
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||||
|
const WpViewportInterfaceName = "wp_viewport"
|
||||||
|
|
||||||
|
// WpViewport : crop and scale interface to a wl_surface
|
||||||
|
//
|
||||||
|
// An additional interface to a wl_surface object, which allows the
|
||||||
|
// client to specify the cropping and scaling of the surface
|
||||||
|
// contents.
|
||||||
|
//
|
||||||
|
// This interface works with two concepts: the source rectangle (src_x,
|
||||||
|
// src_y, src_width, src_height), and the destination size (dst_width,
|
||||||
|
// dst_height). The contents of the source rectangle are scaled to the
|
||||||
|
// destination size, and content outside the source rectangle is ignored.
|
||||||
|
// This state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// The two parts of crop and scale state are independent: the source
|
||||||
|
// rectangle, and the destination size. Initially both are unset, that
|
||||||
|
// is, no scaling is applied. The whole of the current wl_buffer is
|
||||||
|
// used as the source, and the surface size is as defined in
|
||||||
|
// wl_surface.attach.
|
||||||
|
//
|
||||||
|
// If the destination size is set, it causes the surface size to become
|
||||||
|
// dst_width, dst_height. The source (rectangle) is scaled to exactly
|
||||||
|
// this size. This overrides whatever the attached wl_buffer size is,
|
||||||
|
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
|
||||||
|
// has no content and therefore no size. Otherwise, the size is always
|
||||||
|
// at least 1x1 in surface local coordinates.
|
||||||
|
//
|
||||||
|
// If the source rectangle is set, it defines what area of the wl_buffer is
|
||||||
|
// taken as the source. If the source rectangle is set and the destination
|
||||||
|
// size is not set, then src_width and src_height must be integers, and the
|
||||||
|
// surface size becomes the source rectangle size. This results in cropping
|
||||||
|
// without scaling. If src_width or src_height are not integers and
|
||||||
|
// destination size is not set, the bad_size protocol error is raised when
|
||||||
|
// the surface state is applied.
|
||||||
|
//
|
||||||
|
// The coordinate transformations from buffer pixel coordinates up to
|
||||||
|
// the surface-local coordinates happen in the following order:
|
||||||
|
// 1. buffer_transform (wl_surface.set_buffer_transform)
|
||||||
|
// 2. buffer_scale (wl_surface.set_buffer_scale)
|
||||||
|
// 3. crop and scale (wp_viewport.set*)
|
||||||
|
// This means, that the source rectangle coordinates of crop and scale
|
||||||
|
// are given in the coordinates after the buffer transform and scale,
|
||||||
|
// i.e. in the coordinates that would be the surface-local coordinates
|
||||||
|
// if the crop and scale was not applied.
|
||||||
|
//
|
||||||
|
// If src_x or src_y are negative, the bad_value protocol error is raised.
|
||||||
|
// Otherwise, if the source rectangle is partially or completely outside of
|
||||||
|
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
|
||||||
|
// when the surface state is applied. A NULL wl_buffer does not raise the
|
||||||
|
// out_of_buffer error.
|
||||||
|
//
|
||||||
|
// If the wl_surface associated with the wp_viewport is destroyed,
|
||||||
|
// all wp_viewport requests except 'destroy' raise the protocol error
|
||||||
|
// no_surface.
|
||||||
|
//
|
||||||
|
// If the wp_viewport object is destroyed, the crop and scale
|
||||||
|
// state is removed from the wl_surface. The change will be applied
|
||||||
|
// on the next wl_surface.commit.
|
||||||
|
type WpViewport struct {
|
||||||
|
client.BaseProxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWpViewport : crop and scale interface to a wl_surface
|
||||||
|
//
|
||||||
|
// An additional interface to a wl_surface object, which allows the
|
||||||
|
// client to specify the cropping and scaling of the surface
|
||||||
|
// contents.
|
||||||
|
//
|
||||||
|
// This interface works with two concepts: the source rectangle (src_x,
|
||||||
|
// src_y, src_width, src_height), and the destination size (dst_width,
|
||||||
|
// dst_height). The contents of the source rectangle are scaled to the
|
||||||
|
// destination size, and content outside the source rectangle is ignored.
|
||||||
|
// This state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// The two parts of crop and scale state are independent: the source
|
||||||
|
// rectangle, and the destination size. Initially both are unset, that
|
||||||
|
// is, no scaling is applied. The whole of the current wl_buffer is
|
||||||
|
// used as the source, and the surface size is as defined in
|
||||||
|
// wl_surface.attach.
|
||||||
|
//
|
||||||
|
// If the destination size is set, it causes the surface size to become
|
||||||
|
// dst_width, dst_height. The source (rectangle) is scaled to exactly
|
||||||
|
// this size. This overrides whatever the attached wl_buffer size is,
|
||||||
|
// unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface
|
||||||
|
// has no content and therefore no size. Otherwise, the size is always
|
||||||
|
// at least 1x1 in surface local coordinates.
|
||||||
|
//
|
||||||
|
// If the source rectangle is set, it defines what area of the wl_buffer is
|
||||||
|
// taken as the source. If the source rectangle is set and the destination
|
||||||
|
// size is not set, then src_width and src_height must be integers, and the
|
||||||
|
// surface size becomes the source rectangle size. This results in cropping
|
||||||
|
// without scaling. If src_width or src_height are not integers and
|
||||||
|
// destination size is not set, the bad_size protocol error is raised when
|
||||||
|
// the surface state is applied.
|
||||||
|
//
|
||||||
|
// The coordinate transformations from buffer pixel coordinates up to
|
||||||
|
// the surface-local coordinates happen in the following order:
|
||||||
|
// 1. buffer_transform (wl_surface.set_buffer_transform)
|
||||||
|
// 2. buffer_scale (wl_surface.set_buffer_scale)
|
||||||
|
// 3. crop and scale (wp_viewport.set*)
|
||||||
|
// This means, that the source rectangle coordinates of crop and scale
|
||||||
|
// are given in the coordinates after the buffer transform and scale,
|
||||||
|
// i.e. in the coordinates that would be the surface-local coordinates
|
||||||
|
// if the crop and scale was not applied.
|
||||||
|
//
|
||||||
|
// If src_x or src_y are negative, the bad_value protocol error is raised.
|
||||||
|
// Otherwise, if the source rectangle is partially or completely outside of
|
||||||
|
// the non-NULL wl_buffer, then the out_of_buffer protocol error is raised
|
||||||
|
// when the surface state is applied. A NULL wl_buffer does not raise the
|
||||||
|
// out_of_buffer error.
|
||||||
|
//
|
||||||
|
// If the wl_surface associated with the wp_viewport is destroyed,
|
||||||
|
// all wp_viewport requests except 'destroy' raise the protocol error
|
||||||
|
// no_surface.
|
||||||
|
//
|
||||||
|
// If the wp_viewport object is destroyed, the crop and scale
|
||||||
|
// state is removed from the wl_surface. The change will be applied
|
||||||
|
// on the next wl_surface.commit.
|
||||||
|
func NewWpViewport(ctx *client.Context) *WpViewport {
|
||||||
|
wpViewport := &WpViewport{}
|
||||||
|
ctx.Register(wpViewport)
|
||||||
|
return wpViewport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy : remove scaling and cropping from the surface
|
||||||
|
//
|
||||||
|
// The associated wl_surface's crop and scale state is removed.
|
||||||
|
// The change is applied on the next wl_surface.commit.
|
||||||
|
func (i *WpViewport) Destroy() error {
|
||||||
|
defer i.MarkZombie()
|
||||||
|
const opcode = 0
|
||||||
|
const _reqBufLen = 8
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSource : set the source rectangle for cropping
|
||||||
|
//
|
||||||
|
// Set the source rectangle of the associated wl_surface. See
|
||||||
|
// wp_viewport for the description, and relation to the wl_buffer
|
||||||
|
// size.
|
||||||
|
//
|
||||||
|
// If all of x, y, width and height are -1.0, the source rectangle is
|
||||||
|
// unset instead. Any other set of values where width or height are zero
|
||||||
|
// or negative, or x or y are negative, raise the bad_value protocol
|
||||||
|
// error.
|
||||||
|
//
|
||||||
|
// The crop and scale state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// x: source rectangle x
|
||||||
|
// y: source rectangle y
|
||||||
|
// width: source rectangle width
|
||||||
|
// height: source rectangle height
|
||||||
|
func (i *WpViewport) SetSource(x, y, width, height float64) error {
|
||||||
|
const opcode = 1
|
||||||
|
const _reqBufLen = 8 + 4 + 4 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutFixed(_reqBuf[l:l+4], x)
|
||||||
|
l += 4
|
||||||
|
client.PutFixed(_reqBuf[l:l+4], y)
|
||||||
|
l += 4
|
||||||
|
client.PutFixed(_reqBuf[l:l+4], width)
|
||||||
|
l += 4
|
||||||
|
client.PutFixed(_reqBuf[l:l+4], height)
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDestination : set the surface size for scaling
|
||||||
|
//
|
||||||
|
// Set the destination size of the associated wl_surface. See
|
||||||
|
// wp_viewport for the description, and relation to the wl_buffer
|
||||||
|
// size.
|
||||||
|
//
|
||||||
|
// If width is -1 and height is -1, the destination size is unset
|
||||||
|
// instead. Any other pair of values for width and height that
|
||||||
|
// contains zero or negative values raises the bad_value protocol
|
||||||
|
// error.
|
||||||
|
//
|
||||||
|
// The crop and scale state is double-buffered, see wl_surface.commit.
|
||||||
|
//
|
||||||
|
// width: surface width
|
||||||
|
// height: surface height
|
||||||
|
func (i *WpViewport) SetDestination(width, height int32) error {
|
||||||
|
const opcode = 2
|
||||||
|
const _reqBufLen = 8 + 4 + 4
|
||||||
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
l := 0
|
||||||
|
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(width))
|
||||||
|
l += 4
|
||||||
|
client.PutUint32(_reqBuf[l:l+4], uint32(height))
|
||||||
|
l += 4
|
||||||
|
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type WpViewportError uint32
|
||||||
|
|
||||||
|
// WpViewportError :
|
||||||
|
const (
|
||||||
|
// WpViewportErrorBadValue : negative or zero values in width or height
|
||||||
|
WpViewportErrorBadValue WpViewportError = 0
|
||||||
|
// WpViewportErrorBadSize : destination size is not integer
|
||||||
|
WpViewportErrorBadSize WpViewportError = 1
|
||||||
|
// WpViewportErrorOutOfBuffer : source rectangle extends outside of the content area
|
||||||
|
WpViewportErrorOutOfBuffer WpViewportError = 2
|
||||||
|
// WpViewportErrorNoSurface : the wl_surface was destroyed
|
||||||
|
WpViewportErrorNoSurface WpViewportError = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e WpViewportError) Name() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewportErrorBadValue:
|
||||||
|
return "bad_value"
|
||||||
|
case WpViewportErrorBadSize:
|
||||||
|
return "bad_size"
|
||||||
|
case WpViewportErrorOutOfBuffer:
|
||||||
|
return "out_of_buffer"
|
||||||
|
case WpViewportErrorNoSurface:
|
||||||
|
return "no_surface"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewportError) Value() string {
|
||||||
|
switch e {
|
||||||
|
case WpViewportErrorBadValue:
|
||||||
|
return "0"
|
||||||
|
case WpViewportErrorBadSize:
|
||||||
|
return "1"
|
||||||
|
case WpViewportErrorOutOfBuffer:
|
||||||
|
return "2"
|
||||||
|
case WpViewportErrorNoSurface:
|
||||||
|
return "3"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e WpViewportError) String() string {
|
||||||
|
return e.Name() + "=" + e.Value()
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="keyboard_shortcuts_inhibit_unstable_v1">
|
||||||
|
|
||||||
|
<copyright>
|
||||||
|
Copyright © 2017 Red Hat Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<description summary="Protocol for inhibiting the compositor keyboard shortcuts">
|
||||||
|
This protocol specifies a way for a client to request the compositor
|
||||||
|
to ignore its own keyboard shortcuts for a given seat, so that all
|
||||||
|
key events from that seat get forwarded to a surface.
|
||||||
|
|
||||||
|
Warning! The protocol described in this file is experimental and
|
||||||
|
backward incompatible changes may be made. Backward compatible
|
||||||
|
changes may be added together with the corresponding interface
|
||||||
|
version bump.
|
||||||
|
Backward incompatible changes are done by bumping the version
|
||||||
|
number in the protocol and interface names and resetting the
|
||||||
|
interface version. Once the protocol is to be declared stable,
|
||||||
|
the 'z' prefix and the version number in the protocol and
|
||||||
|
interface names are removed and the interface version number is
|
||||||
|
reset.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<interface name="zwp_keyboard_shortcuts_inhibit_manager_v1" version="1">
|
||||||
|
<description summary="context object for keyboard grab_manager">
|
||||||
|
A global interface used for inhibiting the compositor keyboard shortcuts.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the keyboard shortcuts inhibitor object">
|
||||||
|
Destroy the keyboard shortcuts inhibitor manager.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="inhibit_shortcuts">
|
||||||
|
<description summary="create a new keyboard shortcuts inhibitor object">
|
||||||
|
Create a new keyboard shortcuts inhibitor object associated with
|
||||||
|
the given surface for the given seat.
|
||||||
|
|
||||||
|
If shortcuts are already inhibited for the specified seat and surface,
|
||||||
|
a protocol error "already_inhibited" is raised by the compositor.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="zwp_keyboard_shortcuts_inhibitor_v1"/>
|
||||||
|
<arg name="surface" type="object" interface="wl_surface"
|
||||||
|
summary="the surface that inhibits the keyboard shortcuts behavior"/>
|
||||||
|
<arg name="seat" type="object" interface="wl_seat"
|
||||||
|
summary="the wl_seat for which keyboard shortcuts should be disabled"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="already_inhibited"
|
||||||
|
value="0"
|
||||||
|
summary="the shortcuts are already inhibited for this surface"/>
|
||||||
|
</enum>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="zwp_keyboard_shortcuts_inhibitor_v1" version="1">
|
||||||
|
<description summary="context object for keyboard shortcuts inhibitor">
|
||||||
|
A keyboard shortcuts inhibitor instructs the compositor to ignore
|
||||||
|
its own keyboard shortcuts when the associated surface has keyboard
|
||||||
|
focus. As a result, when the surface has keyboard focus on the given
|
||||||
|
seat, it will receive all key events originating from the specified
|
||||||
|
seat, even those which would normally be caught by the compositor for
|
||||||
|
its own shortcuts.
|
||||||
|
|
||||||
|
The Wayland compositor is however under no obligation to disable
|
||||||
|
all of its shortcuts, and may keep some special key combo for its own
|
||||||
|
use, including but not limited to one allowing the user to forcibly
|
||||||
|
restore normal keyboard events routing in the case of an unwilling
|
||||||
|
client. The compositor may also use the same key combo to reactivate
|
||||||
|
an existing shortcut inhibitor that was previously deactivated on
|
||||||
|
user request.
|
||||||
|
|
||||||
|
When the compositor restores its own keyboard shortcuts, an
|
||||||
|
"inactive" event is emitted to notify the client that the keyboard
|
||||||
|
shortcuts inhibitor is not effectively active for the surface and
|
||||||
|
seat any more, and the client should not expect to receive all
|
||||||
|
keyboard events.
|
||||||
|
|
||||||
|
When the keyboard shortcuts inhibitor is inactive, the client has
|
||||||
|
no way to forcibly reactivate the keyboard shortcuts inhibitor.
|
||||||
|
|
||||||
|
The user can chose to re-enable a previously deactivated keyboard
|
||||||
|
shortcuts inhibitor using any mechanism the compositor may offer,
|
||||||
|
in which case the compositor will send an "active" event to notify
|
||||||
|
the client.
|
||||||
|
|
||||||
|
If the surface is destroyed, unmapped, or loses the seat's keyboard
|
||||||
|
focus, the keyboard shortcuts inhibitor becomes irrelevant and the
|
||||||
|
compositor will restore its own keyboard shortcuts but no "inactive"
|
||||||
|
event is emitted in this case.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the keyboard shortcuts inhibitor object">
|
||||||
|
Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="active">
|
||||||
|
<description summary="shortcuts are inhibited">
|
||||||
|
This event indicates that the shortcut inhibitor is active.
|
||||||
|
|
||||||
|
The compositor sends this event every time compositor shortcuts
|
||||||
|
are inhibited on behalf of the surface. When active, the client
|
||||||
|
may receive input events normally reserved by the compositor
|
||||||
|
(see zwp_keyboard_shortcuts_inhibitor_v1).
|
||||||
|
|
||||||
|
This occurs typically when the initial request "inhibit_shortcuts"
|
||||||
|
first becomes active or when the user instructs the compositor to
|
||||||
|
re-enable and existing shortcuts inhibitor using any mechanism
|
||||||
|
offered by the compositor.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="inactive">
|
||||||
|
<description summary="shortcuts are restored">
|
||||||
|
This event indicates that the shortcuts inhibitor is inactive,
|
||||||
|
normal shortcuts processing is restored by the compositor.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
407
core/internal/proto/xml/wlr-layer-shell-unstable-v1.xml
Normal file
407
core/internal/proto/xml/wlr-layer-shell-unstable-v1.xml
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="wlr_layer_shell_unstable_v1">
|
||||||
|
<copyright>
|
||||||
|
Copyright © 2017 Drew DeVault
|
||||||
|
|
||||||
|
Permission to use, copy, modify, distribute, and sell this
|
||||||
|
software and its documentation for any purpose is hereby granted
|
||||||
|
without fee, provided that the above copyright notice appear in
|
||||||
|
all copies and that both that copyright notice and this permission
|
||||||
|
notice appear in supporting documentation, and that the name of
|
||||||
|
the copyright holders not be used in advertising or publicity
|
||||||
|
pertaining to distribution of the software without specific,
|
||||||
|
written prior permission. The copyright holders make no
|
||||||
|
representations about the suitability of this software for any
|
||||||
|
purpose. It is provided "as is" without express or implied
|
||||||
|
warranty.
|
||||||
|
|
||||||
|
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||||
|
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||||
|
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||||
|
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||||
|
THIS SOFTWARE.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<interface name="zwlr_layer_shell_v1" version="5">
|
||||||
|
<description summary="create surfaces that are layers of the desktop">
|
||||||
|
Clients can use this interface to assign the surface_layer role to
|
||||||
|
wl_surfaces. Such surfaces are assigned to a "layer" of the output and
|
||||||
|
rendered with a defined z-depth respective to each other. They may also be
|
||||||
|
anchored to the edges and corners of a screen and specify input handling
|
||||||
|
semantics. This interface should be suitable for the implementation of
|
||||||
|
many desktop shell components, and a broad number of other applications
|
||||||
|
that interact with the desktop.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="get_layer_surface">
|
||||||
|
<description summary="create a layer_surface from a surface">
|
||||||
|
Create a layer surface for an existing surface. This assigns the role of
|
||||||
|
layer_surface, or raises a protocol error if another role is already
|
||||||
|
assigned.
|
||||||
|
|
||||||
|
Creating a layer surface from a wl_surface which has a buffer attached
|
||||||
|
or committed is a client error, and any attempts by a client to attach
|
||||||
|
or manipulate a buffer prior to the first layer_surface.configure call
|
||||||
|
must also be treated as errors.
|
||||||
|
|
||||||
|
After creating a layer_surface object and setting it up, the client
|
||||||
|
must perform an initial commit without any buffer attached.
|
||||||
|
The compositor will reply with a layer_surface.configure event.
|
||||||
|
The client must acknowledge it and is then allowed to attach a buffer
|
||||||
|
to map the surface.
|
||||||
|
|
||||||
|
You may pass NULL for output to allow the compositor to decide which
|
||||||
|
output to use. Generally this will be the one that the user most
|
||||||
|
recently interacted with.
|
||||||
|
|
||||||
|
Clients can specify a namespace that defines the purpose of the layer
|
||||||
|
surface.
|
||||||
|
</description>
|
||||||
|
<arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
|
||||||
|
<arg name="surface" type="object" interface="wl_surface"/>
|
||||||
|
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
|
||||||
|
<arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
|
||||||
|
<arg name="namespace" type="string" summary="namespace for the layer surface"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="role" value="0" summary="wl_surface has another role"/>
|
||||||
|
<entry name="invalid_layer" value="1" summary="layer value is invalid"/>
|
||||||
|
<entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="layer">
|
||||||
|
<description summary="available layers for surfaces">
|
||||||
|
These values indicate which layers a surface can be rendered in. They
|
||||||
|
are ordered by z depth, bottom-most first. Traditional shell surfaces
|
||||||
|
will typically be rendered between the bottom and top layers.
|
||||||
|
Fullscreen shell surfaces are typically rendered at the top layer.
|
||||||
|
Multiple surfaces can share a single layer, and ordering within a
|
||||||
|
single layer is undefined.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<entry name="background" value="0"/>
|
||||||
|
<entry name="bottom" value="1"/>
|
||||||
|
<entry name="top" value="2"/>
|
||||||
|
<entry name="overlay" value="3"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<!-- Version 3 additions -->
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor" since="3">
|
||||||
|
<description summary="destroy the layer_shell object">
|
||||||
|
This request indicates that the client will not use the layer_shell
|
||||||
|
object any more. Objects that have been created through this instance
|
||||||
|
are not affected.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="zwlr_layer_surface_v1" version="5">
|
||||||
|
<description summary="layer metadata interface">
|
||||||
|
An interface that may be implemented by a wl_surface, for surfaces that
|
||||||
|
are designed to be rendered as a layer of a stacked desktop-like
|
||||||
|
environment.
|
||||||
|
|
||||||
|
Layer surface state (layer, size, anchor, exclusive zone,
|
||||||
|
margin, interactivity) is double-buffered, and will be applied at the
|
||||||
|
time wl_surface.commit of the corresponding wl_surface is called.
|
||||||
|
|
||||||
|
Attaching a null buffer to a layer surface unmaps it.
|
||||||
|
|
||||||
|
Unmapping a layer_surface means that the surface cannot be shown by the
|
||||||
|
compositor until it is explicitly mapped again. The layer_surface
|
||||||
|
returns to the state it had right after layer_shell.get_layer_surface.
|
||||||
|
The client can re-map the surface by performing a commit without any
|
||||||
|
buffer attached, waiting for a configure event and handling it as usual.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="set_size">
|
||||||
|
<description summary="sets the size of the surface">
|
||||||
|
Sets the size of the surface in surface-local coordinates. The
|
||||||
|
compositor will display the surface centered with respect to its
|
||||||
|
anchors.
|
||||||
|
|
||||||
|
If you pass 0 for either value, the compositor will assign it and
|
||||||
|
inform you of the assignment in the configure event. You must set your
|
||||||
|
anchor to opposite edges in the dimensions you omit; not doing so is a
|
||||||
|
protocol error. Both values are 0 by default.
|
||||||
|
|
||||||
|
Size is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="width" type="uint"/>
|
||||||
|
<arg name="height" type="uint"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_anchor">
|
||||||
|
<description summary="configures the anchor point of the surface">
|
||||||
|
Requests that the compositor anchor the surface to the specified edges
|
||||||
|
and corners. If two orthogonal edges are specified (e.g. 'top' and
|
||||||
|
'left'), then the anchor point will be the intersection of the edges
|
||||||
|
(e.g. the top left corner of the output); otherwise the anchor point
|
||||||
|
will be centered on that edge, or in the center if none is specified.
|
||||||
|
|
||||||
|
Anchor is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="anchor" type="uint" enum="anchor"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_exclusive_zone">
|
||||||
|
<description summary="configures the exclusive geometry of this surface">
|
||||||
|
Requests that the compositor avoids occluding an area with other
|
||||||
|
surfaces. The compositor's use of this information is
|
||||||
|
implementation-dependent - do not assume that this region will not
|
||||||
|
actually be occluded.
|
||||||
|
|
||||||
|
A positive value is only meaningful if the surface is anchored to one
|
||||||
|
edge or an edge and both perpendicular edges. If the surface is not
|
||||||
|
anchored, anchored to only two perpendicular edges (a corner), anchored
|
||||||
|
to only two parallel edges or anchored to all edges, a positive value
|
||||||
|
will be treated the same as zero.
|
||||||
|
|
||||||
|
A positive zone is the distance from the edge in surface-local
|
||||||
|
coordinates to consider exclusive.
|
||||||
|
|
||||||
|
Surfaces that do not wish to have an exclusive zone may instead specify
|
||||||
|
how they should interact with surfaces that do. If set to zero, the
|
||||||
|
surface indicates that it would like to be moved to avoid occluding
|
||||||
|
surfaces with a positive exclusive zone. If set to -1, the surface
|
||||||
|
indicates that it would not like to be moved to accommodate for other
|
||||||
|
surfaces, and the compositor should extend it all the way to the edges
|
||||||
|
it is anchored to.
|
||||||
|
|
||||||
|
For example, a panel might set its exclusive zone to 10, so that
|
||||||
|
maximized shell surfaces are not shown on top of it. A notification
|
||||||
|
might set its exclusive zone to 0, so that it is moved to avoid
|
||||||
|
occluding the panel, but shell surfaces are shown underneath it. A
|
||||||
|
wallpaper or lock screen might set their exclusive zone to -1, so that
|
||||||
|
they stretch below or over the panel.
|
||||||
|
|
||||||
|
The default value is 0.
|
||||||
|
|
||||||
|
Exclusive zone is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="zone" type="int"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="set_margin">
|
||||||
|
<description summary="sets a margin from the anchor point">
|
||||||
|
Requests that the surface be placed some distance away from the anchor
|
||||||
|
point on the output, in surface-local coordinates. Setting this value
|
||||||
|
for edges you are not anchored to has no effect.
|
||||||
|
|
||||||
|
The exclusive zone includes the margin.
|
||||||
|
|
||||||
|
Margin is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="top" type="int"/>
|
||||||
|
<arg name="right" type="int"/>
|
||||||
|
<arg name="bottom" type="int"/>
|
||||||
|
<arg name="left" type="int"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="keyboard_interactivity">
|
||||||
|
<description summary="types of keyboard interaction possible for a layer shell surface">
|
||||||
|
Types of keyboard interaction possible for layer shell surfaces. The
|
||||||
|
rationale for this is twofold: (1) some applications are not interested
|
||||||
|
in keyboard events and not allowing them to be focused can improve the
|
||||||
|
desktop experience; (2) some applications will want to take exclusive
|
||||||
|
keyboard focus.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<entry name="none" value="0">
|
||||||
|
<description summary="no keyboard focus is possible">
|
||||||
|
This value indicates that this surface is not interested in keyboard
|
||||||
|
events and the compositor should never assign it the keyboard focus.
|
||||||
|
|
||||||
|
This is the default value, set for newly created layer shell surfaces.
|
||||||
|
|
||||||
|
This is useful for e.g. desktop widgets that display information or
|
||||||
|
only have interaction with non-keyboard input devices.
|
||||||
|
</description>
|
||||||
|
</entry>
|
||||||
|
<entry name="exclusive" value="1">
|
||||||
|
<description summary="request exclusive keyboard focus">
|
||||||
|
Request exclusive keyboard focus if this surface is above the shell surface layer.
|
||||||
|
|
||||||
|
For the top and overlay layers, the seat will always give
|
||||||
|
exclusive keyboard focus to the top-most layer which has keyboard
|
||||||
|
interactivity set to exclusive. If this layer contains multiple
|
||||||
|
surfaces with keyboard interactivity set to exclusive, the compositor
|
||||||
|
determines the one receiving keyboard events in an implementation-
|
||||||
|
defined manner. In this case, no guarantee is made when this surface
|
||||||
|
will receive keyboard focus (if ever).
|
||||||
|
|
||||||
|
For the bottom and background layers, the compositor is allowed to use
|
||||||
|
normal focus semantics.
|
||||||
|
|
||||||
|
This setting is mainly intended for applications that need to ensure
|
||||||
|
they receive all keyboard events, such as a lock screen or a password
|
||||||
|
prompt.
|
||||||
|
</description>
|
||||||
|
</entry>
|
||||||
|
<entry name="on_demand" value="2" since="4">
|
||||||
|
<description summary="request regular keyboard focus semantics">
|
||||||
|
This requests the compositor to allow this surface to be focused and
|
||||||
|
unfocused by the user in an implementation-defined manner. The user
|
||||||
|
should be able to unfocus this surface even regardless of the layer
|
||||||
|
it is on.
|
||||||
|
|
||||||
|
Typically, the compositor will want to use its normal mechanism to
|
||||||
|
manage keyboard focus between layer shell surfaces with this setting
|
||||||
|
and regular toplevels on the desktop layer (e.g. click to focus).
|
||||||
|
Nevertheless, it is possible for a compositor to require a special
|
||||||
|
interaction to focus or unfocus layer shell surfaces (e.g. requiring
|
||||||
|
a click even if focus follows the mouse normally, or providing a
|
||||||
|
keybinding to switch focus between layers).
|
||||||
|
|
||||||
|
This setting is mainly intended for desktop shell components (e.g.
|
||||||
|
panels) that allow keyboard interaction. Using this option can allow
|
||||||
|
implementing a desktop shell that can be fully usable without the
|
||||||
|
mouse.
|
||||||
|
</description>
|
||||||
|
</entry>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<request name="set_keyboard_interactivity">
|
||||||
|
<description summary="requests keyboard events">
|
||||||
|
Set how keyboard events are delivered to this surface. By default,
|
||||||
|
layer shell surfaces do not receive keyboard events; this request can
|
||||||
|
be used to change this.
|
||||||
|
|
||||||
|
This setting is inherited by child surfaces set by the get_popup
|
||||||
|
request.
|
||||||
|
|
||||||
|
Layer surfaces receive pointer, touch, and tablet events normally. If
|
||||||
|
you do not want to receive them, set the input region on your surface
|
||||||
|
to an empty region.
|
||||||
|
|
||||||
|
Keyboard interactivity is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="get_popup">
|
||||||
|
<description summary="assign this layer_surface as an xdg_popup parent">
|
||||||
|
This assigns an xdg_popup's parent to this layer_surface. This popup
|
||||||
|
should have been created via xdg_surface::get_popup with the parent set
|
||||||
|
to NULL, and this request must be invoked before committing the popup's
|
||||||
|
initial state.
|
||||||
|
|
||||||
|
See the documentation of xdg_popup for more details about what an
|
||||||
|
xdg_popup is and how it is used.
|
||||||
|
</description>
|
||||||
|
<arg name="popup" type="object" interface="xdg_popup"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="ack_configure">
|
||||||
|
<description summary="ack a configure event">
|
||||||
|
When a configure event is received, if a client commits the
|
||||||
|
surface in response to the configure event, then the client
|
||||||
|
must make an ack_configure request sometime before the commit
|
||||||
|
request, passing along the serial of the configure event.
|
||||||
|
|
||||||
|
If the client receives multiple configure events before it
|
||||||
|
can respond to one, it only has to ack the last configure event.
|
||||||
|
|
||||||
|
A client is not required to commit immediately after sending
|
||||||
|
an ack_configure request - it may even ack_configure several times
|
||||||
|
before its next surface commit.
|
||||||
|
|
||||||
|
A client may send multiple ack_configure requests before committing, but
|
||||||
|
only the last request sent before a commit indicates which configure
|
||||||
|
event the client really is responding to.
|
||||||
|
</description>
|
||||||
|
<arg name="serial" type="uint" summary="the serial from the configure event"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the layer_surface">
|
||||||
|
This request destroys the layer surface.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="configure">
|
||||||
|
<description summary="suggest a surface change">
|
||||||
|
The configure event asks the client to resize its surface.
|
||||||
|
|
||||||
|
Clients should arrange their surface for the new states, and then send
|
||||||
|
an ack_configure request with the serial sent in this configure event at
|
||||||
|
some point before committing the new surface.
|
||||||
|
|
||||||
|
The client is free to dismiss all but the last configure event it
|
||||||
|
received.
|
||||||
|
|
||||||
|
The width and height arguments specify the size of the window in
|
||||||
|
surface-local coordinates.
|
||||||
|
|
||||||
|
The size is a hint, in the sense that the client is free to ignore it if
|
||||||
|
it doesn't resize, pick a smaller size (to satisfy aspect ratio or
|
||||||
|
resize in steps of NxM pixels). If the client picks a smaller size and
|
||||||
|
is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
|
||||||
|
surface will be centered on this axis.
|
||||||
|
|
||||||
|
If the width or height arguments are zero, it means the client should
|
||||||
|
decide its own window dimension.
|
||||||
|
</description>
|
||||||
|
<arg name="serial" type="uint"/>
|
||||||
|
<arg name="width" type="uint"/>
|
||||||
|
<arg name="height" type="uint"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="closed">
|
||||||
|
<description summary="surface should be closed">
|
||||||
|
The closed event is sent by the compositor when the surface will no
|
||||||
|
longer be shown. The output may have been destroyed or the user may
|
||||||
|
have asked for it to be removed. Further changes to the surface will be
|
||||||
|
ignored. The client should destroy the resource after receiving this
|
||||||
|
event, and create a new surface if they so choose.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
|
||||||
|
<entry name="invalid_size" value="1" summary="size is invalid"/>
|
||||||
|
<entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
|
||||||
|
<entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
|
||||||
|
<entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="anchor" bitfield="true">
|
||||||
|
<entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
|
||||||
|
<entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
|
||||||
|
<entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
|
||||||
|
<entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<!-- Version 2 additions -->
|
||||||
|
|
||||||
|
<request name="set_layer" since="2">
|
||||||
|
<description summary="change the layer of the surface">
|
||||||
|
Change the layer that the surface is rendered on.
|
||||||
|
|
||||||
|
Layer is double-buffered, see wl_surface.commit.
|
||||||
|
</description>
|
||||||
|
<arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<!-- Version 5 additions -->
|
||||||
|
|
||||||
|
<request name="set_exclusive_edge" since="5">
|
||||||
|
<description summary="set the edge the exclusive zone will be applied to">
|
||||||
|
Requests an edge for the exclusive zone to apply. The exclusive
|
||||||
|
edge will be automatically deduced from anchor points when possible,
|
||||||
|
but when the surface is anchored to a corner, it will be necessary
|
||||||
|
to set it explicitly to disambiguate, as it is not possible to deduce
|
||||||
|
which one of the two corner edges should be used.
|
||||||
|
|
||||||
|
The edge must be one the surface is anchored to, otherwise the
|
||||||
|
invalid_exclusive_edge protocol error will be raised.
|
||||||
|
</description>
|
||||||
|
<arg name="edge" type="uint" enum="anchor"/>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
234
core/internal/proto/xml/wlr-screencopy-unstable-v1.xml
Normal file
234
core/internal/proto/xml/wlr-screencopy-unstable-v1.xml
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<protocol name="wlr_screencopy_unstable_v1">
|
||||||
|
<copyright>
|
||||||
|
Copyright © 2018 Simon Ser
|
||||||
|
Copyright © 2019 Andri Yngvason
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
</copyright>
|
||||||
|
|
||||||
|
<description summary="screen content capturing on client buffers">
|
||||||
|
This protocol allows clients to ask the compositor to copy part of the
|
||||||
|
screen content to a client buffer.
|
||||||
|
|
||||||
|
Warning! The protocol described in this file is experimental and
|
||||||
|
backward incompatible changes may be made. Backward compatible changes
|
||||||
|
may be added together with the corresponding interface version bump.
|
||||||
|
Backward incompatible changes are done by bumping the version number in
|
||||||
|
the protocol and interface names and resetting the interface version.
|
||||||
|
Once the protocol is to be declared stable, the 'z' prefix and the
|
||||||
|
version number in the protocol and interface names are removed and the
|
||||||
|
interface version number is reset.
|
||||||
|
|
||||||
|
Note! This protocol is deprecated and not intended for production use.
|
||||||
|
The ext-image-copy-capture-v1 protocol should be used instead.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<interface name="zwlr_screencopy_manager_v1" version="3">
|
||||||
|
<description summary="manager to inform clients and begin capturing">
|
||||||
|
This object is a manager which offers requests to start capturing from a
|
||||||
|
source.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<request name="capture_output">
|
||||||
|
<description summary="capture an output">
|
||||||
|
Capture the next frame of an entire output.
|
||||||
|
</description>
|
||||||
|
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||||
|
<arg name="overlay_cursor" type="int"
|
||||||
|
summary="composite cursor onto the frame"/>
|
||||||
|
<arg name="output" type="object" interface="wl_output"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="capture_output_region">
|
||||||
|
<description summary="capture an output's region">
|
||||||
|
Capture the next frame of an output's region.
|
||||||
|
|
||||||
|
The region is given in output logical coordinates, see
|
||||||
|
xdg_output.logical_size. The region will be clipped to the output's
|
||||||
|
extents.
|
||||||
|
</description>
|
||||||
|
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
|
||||||
|
<arg name="overlay_cursor" type="int"
|
||||||
|
summary="composite cursor onto the frame"/>
|
||||||
|
<arg name="output" type="object" interface="wl_output"/>
|
||||||
|
<arg name="x" type="int"/>
|
||||||
|
<arg name="y" type="int"/>
|
||||||
|
<arg name="width" type="int"/>
|
||||||
|
<arg name="height" type="int"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="destroy the manager">
|
||||||
|
All objects created by the manager will still remain valid, until their
|
||||||
|
appropriate destroy request has been called.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
</interface>
|
||||||
|
|
||||||
|
<interface name="zwlr_screencopy_frame_v1" version="3">
|
||||||
|
<description summary="a frame ready for copy">
|
||||||
|
This object represents a single frame.
|
||||||
|
|
||||||
|
When created, a series of buffer events will be sent, each representing a
|
||||||
|
supported buffer type. The "buffer_done" event is sent afterwards to
|
||||||
|
indicate that all supported buffer types have been enumerated. The client
|
||||||
|
will then be able to send a "copy" request. If the capture is successful,
|
||||||
|
the compositor will send a "flags" event followed by a "ready" event.
|
||||||
|
|
||||||
|
For objects version 2 or lower, wl_shm buffers are always supported, ie.
|
||||||
|
the "buffer" event is guaranteed to be sent.
|
||||||
|
|
||||||
|
If the capture failed, the "failed" event is sent. This can happen anytime
|
||||||
|
before the "ready" event.
|
||||||
|
|
||||||
|
Once either a "ready" or a "failed" event is received, the client should
|
||||||
|
destroy the frame.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<event name="buffer">
|
||||||
|
<description summary="wl_shm buffer information">
|
||||||
|
Provides information about wl_shm buffer parameters that need to be
|
||||||
|
used for this frame. This event is sent once after the frame is created
|
||||||
|
if wl_shm buffers are supported.
|
||||||
|
</description>
|
||||||
|
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
|
||||||
|
<arg name="width" type="uint" summary="buffer width"/>
|
||||||
|
<arg name="height" type="uint" summary="buffer height"/>
|
||||||
|
<arg name="stride" type="uint" summary="buffer stride"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<request name="copy">
|
||||||
|
<description summary="copy the frame">
|
||||||
|
Copy the frame to the supplied buffer. The buffer must have the
|
||||||
|
correct size, see zwlr_screencopy_frame_v1.buffer and
|
||||||
|
zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
|
||||||
|
supported format.
|
||||||
|
|
||||||
|
If the frame is successfully copied, "flags" and "ready" events are
|
||||||
|
sent. Otherwise, a "failed" event is sent.
|
||||||
|
</description>
|
||||||
|
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<enum name="error">
|
||||||
|
<entry name="already_used" value="0"
|
||||||
|
summary="the object has already been used to copy a wl_buffer"/>
|
||||||
|
<entry name="invalid_buffer" value="1"
|
||||||
|
summary="buffer attributes are invalid"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<enum name="flags" bitfield="true">
|
||||||
|
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
|
||||||
|
</enum>
|
||||||
|
|
||||||
|
<event name="flags">
|
||||||
|
<description summary="frame flags">
|
||||||
|
Provides flags about the frame. This event is sent once before the
|
||||||
|
"ready" event.
|
||||||
|
</description>
|
||||||
|
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="ready">
|
||||||
|
<description summary="indicates frame is available for reading">
|
||||||
|
Called as soon as the frame is copied, indicating it is available
|
||||||
|
for reading. This event includes the time at which the presentation took place.
|
||||||
|
|
||||||
|
The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
|
||||||
|
each component being an unsigned 32-bit value. Whole seconds are in
|
||||||
|
tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
|
||||||
|
and the additional fractional part in tv_nsec as nanoseconds. Hence,
|
||||||
|
for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
|
||||||
|
may have an arbitrary offset at start.
|
||||||
|
|
||||||
|
After receiving this event, the client should destroy the object.
|
||||||
|
</description>
|
||||||
|
<arg name="tv_sec_hi" type="uint"
|
||||||
|
summary="high 32 bits of the seconds part of the timestamp"/>
|
||||||
|
<arg name="tv_sec_lo" type="uint"
|
||||||
|
summary="low 32 bits of the seconds part of the timestamp"/>
|
||||||
|
<arg name="tv_nsec" type="uint"
|
||||||
|
summary="nanoseconds part of the timestamp"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="failed">
|
||||||
|
<description summary="frame copy failed">
|
||||||
|
This event indicates that the attempted frame copy has failed.
|
||||||
|
|
||||||
|
After receiving this event, the client should destroy the object.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<request name="destroy" type="destructor">
|
||||||
|
<description summary="delete this object, used or not">
|
||||||
|
Destroys the frame. This request can be sent at any time by the client.
|
||||||
|
</description>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<!-- Version 2 additions -->
|
||||||
|
<request name="copy_with_damage" since="2">
|
||||||
|
<description summary="copy the frame when it's damaged">
|
||||||
|
Same as copy, except it waits until there is damage to copy.
|
||||||
|
</description>
|
||||||
|
<arg name="buffer" type="object" interface="wl_buffer"/>
|
||||||
|
</request>
|
||||||
|
|
||||||
|
<event name="damage" since="2">
|
||||||
|
<description summary="carries the coordinates of the damaged region">
|
||||||
|
This event is sent right before the ready event when copy_with_damage is
|
||||||
|
requested. It may be generated multiple times for each copy_with_damage
|
||||||
|
request.
|
||||||
|
|
||||||
|
The arguments describe a box around an area that has changed since the
|
||||||
|
last copy request that was derived from the current screencopy manager
|
||||||
|
instance.
|
||||||
|
|
||||||
|
The union of all regions received between the call to copy_with_damage
|
||||||
|
and a ready event is the total damage since the prior ready event.
|
||||||
|
</description>
|
||||||
|
<arg name="x" type="uint" summary="damaged x coordinates"/>
|
||||||
|
<arg name="y" type="uint" summary="damaged y coordinates"/>
|
||||||
|
<arg name="width" type="uint" summary="current width"/>
|
||||||
|
<arg name="height" type="uint" summary="current height"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<!-- Version 3 additions -->
|
||||||
|
<event name="linux_dmabuf" since="3">
|
||||||
|
<description summary="linux-dmabuf buffer information">
|
||||||
|
Provides information about linux-dmabuf buffer parameters that need to
|
||||||
|
be used for this frame. This event is sent once after the frame is
|
||||||
|
created if linux-dmabuf buffers are supported.
|
||||||
|
</description>
|
||||||
|
<arg name="format" type="uint" summary="fourcc pixel format"/>
|
||||||
|
<arg name="width" type="uint" summary="buffer width"/>
|
||||||
|
<arg name="height" type="uint" summary="buffer height"/>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
<event name="buffer_done" since="3">
|
||||||
|
<description summary="all buffer types reported">
|
||||||
|
This event is sent once after all buffer events have been sent.
|
||||||
|
|
||||||
|
The client should proceed to create a buffer of one of the supported
|
||||||
|
types, and send a "copy" request.
|
||||||
|
</description>
|
||||||
|
</event>
|
||||||
|
</interface>
|
||||||
|
</protocol>
|
||||||
523
core/internal/screenshot/compositor.go
Normal file
523
core/internal/screenshot/compositor.go
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Compositor int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompositorUnknown Compositor = iota
|
||||||
|
CompositorHyprland
|
||||||
|
CompositorSway
|
||||||
|
CompositorNiri
|
||||||
|
CompositorDWL
|
||||||
|
)
|
||||||
|
|
||||||
|
var detectedCompositor Compositor = -1
|
||||||
|
|
||||||
|
func DetectCompositor() Compositor {
|
||||||
|
if detectedCompositor >= 0 {
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
hyprlandSig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
||||||
|
niriSocket := os.Getenv("NIRI_SOCKET")
|
||||||
|
swaySocket := os.Getenv("SWAYSOCK")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case niriSocket != "":
|
||||||
|
if _, err := os.Stat(niriSocket); err == nil {
|
||||||
|
detectedCompositor = CompositorNiri
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
case swaySocket != "":
|
||||||
|
if _, err := os.Stat(swaySocket); err == nil {
|
||||||
|
detectedCompositor = CompositorSway
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
case hyprlandSig != "":
|
||||||
|
detectedCompositor = CompositorHyprland
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
if detectDWLProtocol() {
|
||||||
|
detectedCompositor = CompositorDWL
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
detectedCompositor = CompositorUnknown
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectDWLProtocol() bool {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
if e.Interface == dwl_ipc.ZdwlIpcManagerV2InterfaceName {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetCompositorDWL() {
|
||||||
|
detectedCompositor = CompositorDWL
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowGeometry struct {
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
Width int32
|
||||||
|
Height int32
|
||||||
|
Output string
|
||||||
|
Scale float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetActiveWindow() (*WindowGeometry, error) {
|
||||||
|
switch DetectCompositor() {
|
||||||
|
case CompositorHyprland:
|
||||||
|
return getHyprlandActiveWindow()
|
||||||
|
case CompositorDWL:
|
||||||
|
return getDWLActiveWindow()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("window capture requires Hyprland or DWL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hyprlandWindow struct {
|
||||||
|
At [2]int32 `json:"at"`
|
||||||
|
Size [2]int32 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHyprlandActiveWindow() (*WindowGeometry, error) {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "activewindow").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hyprctl activewindow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var win hyprlandWindow
|
||||||
|
if err := json.Unmarshal(output, &win); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse activewindow: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if win.Size[0] <= 0 || win.Size[1] <= 0 {
|
||||||
|
return nil, fmt.Errorf("no active window")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WindowGeometry{
|
||||||
|
X: win.At[0],
|
||||||
|
Y: win.At[1],
|
||||||
|
Width: win.Size[0],
|
||||||
|
Height: win.Size[1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type hyprlandMonitor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
X int32 `json:"x"`
|
||||||
|
Y int32 `json:"y"`
|
||||||
|
Width int32 `json:"width"`
|
||||||
|
Height int32 `json:"height"`
|
||||||
|
Scale float64 `json:"scale"`
|
||||||
|
Focused bool `json:"focused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHyprlandMonitorScale(name string) float64 {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitors []hyprlandMonitor
|
||||||
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range monitors {
|
||||||
|
if m.Name == name {
|
||||||
|
return m.Scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHyprlandFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitors []hyprlandMonitor
|
||||||
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range monitors {
|
||||||
|
if m.Focused {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHyprlandMonitorGeometry(name string) (x, y, w, h int32, ok bool) {
|
||||||
|
output, err := exec.Command("hyprctl", "-j", "monitors").Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitors []hyprlandMonitor
|
||||||
|
if err := json.Unmarshal(output, &monitors); err != nil {
|
||||||
|
return 0, 0, 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range monitors {
|
||||||
|
if m.Name == name {
|
||||||
|
logicalW := int32(float64(m.Width) / m.Scale)
|
||||||
|
logicalH := int32(float64(m.Height) / m.Scale)
|
||||||
|
return m.X, m.Y, logicalW, logicalH, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, 0, 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type swayWorkspace struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
Focused bool `json:"focused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSwayFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("swaymsg", "-t", "get_workspaces").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var workspaces []swayWorkspace
|
||||||
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
if ws.Focused {
|
||||||
|
return ws.Output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type niriWorkspace struct {
|
||||||
|
Output string `json:"output"`
|
||||||
|
IsFocused bool `json:"is_focused"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNiriFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("niri", "msg", "-j", "workspaces").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var workspaces []niriWorkspace
|
||||||
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
if ws.IsFocused {
|
||||||
|
return ws.Output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlActiveOutput string
|
||||||
|
|
||||||
|
func SetDWLActiveOutput(name string) {
|
||||||
|
dwlActiveOutput = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDWLFocusedMonitor() string {
|
||||||
|
if dwlActiveOutput != "" {
|
||||||
|
return dwlActiveOutput
|
||||||
|
}
|
||||||
|
return queryDWLActiveOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryDWLActiveOutput() string {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
|
||||||
|
outputs := make(map[uint32]*client.Output)
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
|
||||||
|
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
dwlManager = mgr
|
||||||
|
}
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
out := client.NewOutput(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
|
||||||
|
outputs[e.Name] = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if dwlManager == nil || len(outputs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
outputNames := make(map[uint32]string)
|
||||||
|
for name, out := range outputs {
|
||||||
|
n := name
|
||||||
|
out.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
outputNames[n] = e.Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type outputState struct {
|
||||||
|
name string
|
||||||
|
active bool
|
||||||
|
gotFrame bool
|
||||||
|
}
|
||||||
|
states := make(map[uint32]*outputState)
|
||||||
|
|
||||||
|
for name, out := range outputs {
|
||||||
|
dwlOut, err := dwlManager.GetOutput(out)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state := &outputState{name: outputNames[name]}
|
||||||
|
states[name] = state
|
||||||
|
|
||||||
|
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
|
||||||
|
state.active = e.Active != 0
|
||||||
|
})
|
||||||
|
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
|
||||||
|
state.gotFrame = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
allFramesReceived := func() bool {
|
||||||
|
for _, s := range states {
|
||||||
|
if !s.gotFrame {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for !allFramesReceived() {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range states {
|
||||||
|
if state.active {
|
||||||
|
return state.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFocusedMonitor() string {
|
||||||
|
switch DetectCompositor() {
|
||||||
|
case CompositorHyprland:
|
||||||
|
return getHyprlandFocusedMonitor()
|
||||||
|
case CompositorSway:
|
||||||
|
return getSwayFocusedMonitor()
|
||||||
|
case CompositorNiri:
|
||||||
|
return getNiriFocusedMonitor()
|
||||||
|
case CompositorDWL:
|
||||||
|
return getDWLFocusedMonitor()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect: %w", err)
|
||||||
|
}
|
||||||
|
ctx := display.Context()
|
||||||
|
defer ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
|
||||||
|
outputs := make(map[uint32]*client.Output)
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
|
||||||
|
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
dwlManager = mgr
|
||||||
|
}
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
out := client.NewOutput(ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
|
||||||
|
outputs[e.Name] = out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dwlManager == nil {
|
||||||
|
return nil, fmt.Errorf("dwl_ipc_manager not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outputs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no outputs found")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputNames := make(map[uint32]string)
|
||||||
|
for name, out := range outputs {
|
||||||
|
n := name
|
||||||
|
out.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
outputNames[n] = e.Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("roundtrip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dwlOutputState struct {
|
||||||
|
output *dwl_ipc.ZdwlIpcOutputV2
|
||||||
|
name string
|
||||||
|
active bool
|
||||||
|
x, y int32
|
||||||
|
w, h int32
|
||||||
|
scalefactor uint32
|
||||||
|
gotFrame bool
|
||||||
|
}
|
||||||
|
|
||||||
|
dwlOutputs := make(map[uint32]*dwlOutputState)
|
||||||
|
for name, out := range outputs {
|
||||||
|
dwlOut, err := dwlManager.GetOutput(out)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state := &dwlOutputState{output: dwlOut, name: outputNames[name]}
|
||||||
|
dwlOutputs[name] = state
|
||||||
|
|
||||||
|
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
|
||||||
|
state.active = e.Active != 0
|
||||||
|
})
|
||||||
|
dwlOut.SetXHandler(func(e dwl_ipc.ZdwlIpcOutputV2XEvent) {
|
||||||
|
state.x = e.X
|
||||||
|
})
|
||||||
|
dwlOut.SetYHandler(func(e dwl_ipc.ZdwlIpcOutputV2YEvent) {
|
||||||
|
state.y = e.Y
|
||||||
|
})
|
||||||
|
dwlOut.SetWidthHandler(func(e dwl_ipc.ZdwlIpcOutputV2WidthEvent) {
|
||||||
|
state.w = e.Width
|
||||||
|
})
|
||||||
|
dwlOut.SetHeightHandler(func(e dwl_ipc.ZdwlIpcOutputV2HeightEvent) {
|
||||||
|
state.h = e.Height
|
||||||
|
})
|
||||||
|
dwlOut.SetScalefactorHandler(func(e dwl_ipc.ZdwlIpcOutputV2ScalefactorEvent) {
|
||||||
|
state.scalefactor = e.Scalefactor
|
||||||
|
})
|
||||||
|
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
|
||||||
|
state.gotFrame = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
allFramesReceived := func() bool {
|
||||||
|
for _, s := range dwlOutputs {
|
||||||
|
if !s.gotFrame {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for !allFramesReceived() {
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil, fmt.Errorf("dispatch: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state := range dwlOutputs {
|
||||||
|
if !state.active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if state.w <= 0 || state.h <= 0 {
|
||||||
|
return nil, fmt.Errorf("no active window")
|
||||||
|
}
|
||||||
|
scale := float64(state.scalefactor) / 100.0
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
return &WindowGeometry{
|
||||||
|
X: state.x,
|
||||||
|
Y: state.y,
|
||||||
|
Width: state.w,
|
||||||
|
Height: state.h,
|
||||||
|
Output: state.name,
|
||||||
|
Scale: scale,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no active output found")
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
809
core/internal/screenshot/region.go
Normal file
809
core/internal/screenshot/region.go
Normal file
@@ -0,0 +1,809 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SelectionState struct {
|
||||||
|
hasSelection bool // There's a selection to display (pre-loaded or user-drawn)
|
||||||
|
dragging bool // User is actively drawing a new selection
|
||||||
|
surface *OutputSurface // Surface where selection was made
|
||||||
|
// Surface-local logical coordinates (from pointer events)
|
||||||
|
anchorX float64
|
||||||
|
anchorY float64
|
||||||
|
currentX float64
|
||||||
|
currentY float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderSlot struct {
|
||||||
|
shm *ShmBuffer
|
||||||
|
pool *client.ShmPool
|
||||||
|
wlBuf *client.Buffer
|
||||||
|
busy bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputSurface struct {
|
||||||
|
output *WaylandOutput
|
||||||
|
wlSurface *client.Surface
|
||||||
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
|
viewport *wp_viewporter.WpViewport
|
||||||
|
screenBuf *ShmBuffer
|
||||||
|
screenBufNoCursor *ShmBuffer
|
||||||
|
screenFormat uint32
|
||||||
|
logicalW int
|
||||||
|
logicalH int
|
||||||
|
configured bool
|
||||||
|
yInverted bool
|
||||||
|
|
||||||
|
// Triple-buffered render slots
|
||||||
|
slots [3]*RenderSlot
|
||||||
|
slotsReady bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreCapture struct {
|
||||||
|
screenBuf *ShmBuffer
|
||||||
|
screenBufNoCursor *ShmBuffer
|
||||||
|
format uint32
|
||||||
|
yInverted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionSelector struct {
|
||||||
|
screenshoter *Screenshoter
|
||||||
|
|
||||||
|
display *client.Display
|
||||||
|
registry *client.Registry
|
||||||
|
ctx *client.Context
|
||||||
|
|
||||||
|
compositor *client.Compositor
|
||||||
|
shm *client.Shm
|
||||||
|
seat *client.Seat
|
||||||
|
pointer *client.Pointer
|
||||||
|
keyboard *client.Keyboard
|
||||||
|
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||||
|
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||||
|
viewporter *wp_viewporter.WpViewporter
|
||||||
|
|
||||||
|
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
||||||
|
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
||||||
|
|
||||||
|
outputs map[uint32]*WaylandOutput
|
||||||
|
outputsMu sync.Mutex
|
||||||
|
preCapture map[*WaylandOutput]*PreCapture
|
||||||
|
|
||||||
|
surfaces []*OutputSurface
|
||||||
|
activeSurface *OutputSurface
|
||||||
|
|
||||||
|
// Cursor surface for crosshair
|
||||||
|
cursorSurface *client.Surface
|
||||||
|
cursorBuffer *ShmBuffer
|
||||||
|
cursorWlBuf *client.Buffer
|
||||||
|
cursorPool *client.ShmPool
|
||||||
|
|
||||||
|
selection SelectionState
|
||||||
|
pointerX float64
|
||||||
|
pointerY float64
|
||||||
|
preSelect Region
|
||||||
|
showCapturedCursor bool
|
||||||
|
shiftHeld bool
|
||||||
|
|
||||||
|
running bool
|
||||||
|
cancelled bool
|
||||||
|
result Region
|
||||||
|
|
||||||
|
capturedBuffer *ShmBuffer
|
||||||
|
capturedRegion Region
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
||||||
|
return &RegionSelector{
|
||||||
|
screenshoter: s,
|
||||||
|
outputs: make(map[uint32]*WaylandOutput),
|
||||||
|
preCapture: make(map[*WaylandOutput]*PreCapture),
|
||||||
|
showCapturedCursor: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
||||||
|
r.preSelect = GetLastRegion()
|
||||||
|
|
||||||
|
if err := r.connect(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer r.cleanup()
|
||||||
|
|
||||||
|
if err := r.setupRegistry(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("registry setup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.roundtrip(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("roundtrip after registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case r.screencopy == nil:
|
||||||
|
return nil, false, fmt.Errorf("compositor does not support wlr-screencopy-unstable-v1")
|
||||||
|
case r.layerShell == nil:
|
||||||
|
return nil, false, fmt.Errorf("compositor does not support wlr-layer-shell-unstable-v1")
|
||||||
|
case r.seat == nil:
|
||||||
|
return nil, false, fmt.Errorf("no seat available")
|
||||||
|
case r.compositor == nil:
|
||||||
|
return nil, false, fmt.Errorf("compositor not available")
|
||||||
|
case r.shm == nil:
|
||||||
|
return nil, false, fmt.Errorf("wl_shm not available")
|
||||||
|
case len(r.outputs) == 0:
|
||||||
|
return nil, false, fmt.Errorf("no outputs available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.roundtrip(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("roundtrip after protocol check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.preCaptureAllOutputs(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("pre-capture: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.createSurfaces(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("create surfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.createCursor()
|
||||||
|
|
||||||
|
if err := r.roundtrip(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("roundtrip after surfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.running = true
|
||||||
|
for r.running {
|
||||||
|
if err := r.ctx.Dispatch(); err != nil {
|
||||||
|
return nil, false, fmt.Errorf("dispatch: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.cancelled || r.capturedBuffer == nil {
|
||||||
|
return nil, r.cancelled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
yInverted := false
|
||||||
|
var format uint32
|
||||||
|
if r.selection.surface != nil {
|
||||||
|
yInverted = r.selection.surface.yInverted
|
||||||
|
format = r.selection.surface.screenFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: r.capturedBuffer,
|
||||||
|
Region: r.result,
|
||||||
|
YInverted: yInverted,
|
||||||
|
Format: format,
|
||||||
|
}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) connect() error {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.display = display
|
||||||
|
r.ctx = display.Context()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) roundtrip() error {
|
||||||
|
return wlhelpers.Roundtrip(r.display, r.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupRegistry() error {
|
||||||
|
registry, err := r.display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.registry = registry
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
r.handleGlobal(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.SetGlobalRemoveHandler(func(e client.RegistryGlobalRemoveEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
delete(r.outputs, e.Name)
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) handleGlobal(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case client.CompositorInterfaceName:
|
||||||
|
comp := client.NewCompositor(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, comp); err == nil {
|
||||||
|
r.compositor = comp
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.ShmInterfaceName:
|
||||||
|
shm := client.NewShm(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, shm); err == nil {
|
||||||
|
r.shm = shm
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.SeatInterfaceName:
|
||||||
|
seat := client.NewSeat(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, seat); err == nil {
|
||||||
|
r.seat = seat
|
||||||
|
r.setupInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
case client.OutputInterfaceName:
|
||||||
|
output := client.NewOutput(r.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
r.outputs[e.Name] = &WaylandOutput{
|
||||||
|
wlOutput: output,
|
||||||
|
globalName: e.Name,
|
||||||
|
scale: 1,
|
||||||
|
fractionalScale: 1.0,
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
r.setupOutputHandlers(e.Name, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||||
|
ls := wlr_layer_shell.NewZwlrLayerShellV1(r.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 4 {
|
||||||
|
version = 4
|
||||||
|
}
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, version, ls); err == nil {
|
||||||
|
r.layerShell = ls
|
||||||
|
}
|
||||||
|
|
||||||
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
|
sc := wlr_screencopy.NewZwlrScreencopyManagerV1(r.ctx)
|
||||||
|
version := e.Version
|
||||||
|
if version > 3 {
|
||||||
|
version = 3
|
||||||
|
}
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, version, sc); err == nil {
|
||||||
|
r.screencopy = sc
|
||||||
|
}
|
||||||
|
|
||||||
|
case wp_viewporter.WpViewporterInterfaceName:
|
||||||
|
vp := wp_viewporter.NewWpViewporter(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, vp); err == nil {
|
||||||
|
r.viewporter = vp
|
||||||
|
}
|
||||||
|
|
||||||
|
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
||||||
|
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(r.ctx)
|
||||||
|
if err := r.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
|
r.shortcutsInhibitMgr = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupOutputHandlers(name uint32, output *client.Output) {
|
||||||
|
output.SetGeometryHandler(func(e client.OutputGeometryEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.x = e.X
|
||||||
|
o.y = e.Y
|
||||||
|
o.transform = int32(e.Transform)
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetModeHandler(func(e client.OutputModeEvent) {
|
||||||
|
if e.Flags&uint32(client.OutputModeCurrent) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.width = e.Width
|
||||||
|
o.height = e.Height
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetScaleHandler(func(e client.OutputScaleEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.scale = e.Factor
|
||||||
|
o.fractionalScale = float64(e.Factor)
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
output.SetNameHandler(func(e client.OutputNameEvent) {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
if o, ok := r.outputs[name]; ok {
|
||||||
|
o.name = e.Name
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) preCaptureAllOutputs() error {
|
||||||
|
r.outputsMu.Lock()
|
||||||
|
outputs := make([]*WaylandOutput, 0, len(r.outputs))
|
||||||
|
for _, o := range r.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
}
|
||||||
|
r.outputsMu.Unlock()
|
||||||
|
|
||||||
|
pending := len(outputs) * 2
|
||||||
|
done := make(chan struct{}, pending)
|
||||||
|
|
||||||
|
for _, output := range outputs {
|
||||||
|
pc := &PreCapture{}
|
||||||
|
r.preCapture[output] = pc
|
||||||
|
|
||||||
|
r.preCaptureOutput(output, pc, true, func() { done <- struct{}{} })
|
||||||
|
r.preCaptureOutput(output, pc, false, func() { done <- struct{}{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < pending; i++ {
|
||||||
|
if err := r.ctx.Dispatch(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
default:
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) preCaptureOutput(output *WaylandOutput, pc *PreCapture, withCursor bool, onReady func()) {
|
||||||
|
cursor := int32(0)
|
||||||
|
if withCursor {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := r.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("screencopy capture failed", "err", err)
|
||||||
|
onReady()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if os.logicalW > 0 && os.screenBuf != nil {
|
||||||
|
os.output.fractionalScale = float64(os.screenBuf.Width) / float64(os.logicalW)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.initRenderBuffer(os)
|
||||||
|
r.applyPreSelection(os)
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) initRenderBuffer(os *OutputSurface) {
|
||||||
|
if os.screenBuf == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
slot := &RenderSlot{}
|
||||||
|
|
||||||
|
buf, err := CreateShmBuffer(os.screenBuf.Width, os.screenBuf.Height, os.screenBuf.Stride)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create render slot buffer failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slot.shm = buf
|
||||||
|
|
||||||
|
pool, err := r.shm.CreatePool(buf.Fd(), int32(buf.Size()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create render slot pool failed", "err", err)
|
||||||
|
buf.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slot.pool = pool
|
||||||
|
|
||||||
|
wlBuf, err := pool.CreateBuffer(0, int32(buf.Width), int32(buf.Height), int32(buf.Stride), os.screenFormat)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create render slot wl_buffer failed", "err", err)
|
||||||
|
pool.Destroy()
|
||||||
|
buf.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slot.wlBuf = wlBuf
|
||||||
|
|
||||||
|
slotRef := slot
|
||||||
|
wlBuf.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
slotRef.busy = false
|
||||||
|
})
|
||||||
|
|
||||||
|
os.slots[i] = slot
|
||||||
|
}
|
||||||
|
os.slotsReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (os *OutputSurface) acquireFreeSlot() *RenderSlot {
|
||||||
|
for _, slot := range os.slots {
|
||||||
|
if slot != nil && !slot.busy {
|
||||||
|
return slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) applyPreSelection(os *OutputSurface) {
|
||||||
|
if r.preSelect.IsEmpty() || os.screenBuf == nil || r.selection.hasSelection {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.preSelect.Output != "" && r.preSelect.Output != os.output.name {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleX := float64(os.logicalW) / float64(os.screenBuf.Width)
|
||||||
|
scaleY := float64(os.logicalH) / float64(os.screenBuf.Height)
|
||||||
|
|
||||||
|
x1 := float64(r.preSelect.X-os.output.x) * scaleX
|
||||||
|
y1 := float64(r.preSelect.Y-os.output.y) * scaleY
|
||||||
|
x2 := float64(r.preSelect.X-os.output.x+r.preSelect.Width) * scaleX
|
||||||
|
y2 := float64(r.preSelect.Y-os.output.y+r.preSelect.Height) * scaleY
|
||||||
|
|
||||||
|
r.selection.hasSelection = true
|
||||||
|
r.selection.dragging = false
|
||||||
|
r.selection.surface = os
|
||||||
|
r.selection.anchorX = x1
|
||||||
|
r.selection.anchorY = y1
|
||||||
|
r.selection.currentX = x2
|
||||||
|
r.selection.currentY = y2
|
||||||
|
r.activeSurface = os
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) getSourceBuffer(os *OutputSurface) *ShmBuffer {
|
||||||
|
if !r.showCapturedCursor && os.screenBufNoCursor != nil {
|
||||||
|
return os.screenBufNoCursor
|
||||||
|
}
|
||||||
|
return os.screenBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) redrawSurface(os *OutputSurface) {
|
||||||
|
srcBuf := r.getSourceBuffer(os)
|
||||||
|
if srcBuf == nil || !os.slotsReady {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slot := os.acquireFreeSlot()
|
||||||
|
if slot == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.shm.CopyFrom(srcBuf)
|
||||||
|
|
||||||
|
// Draw overlay (dimming + selection) into this slot
|
||||||
|
r.drawOverlay(os, slot.shm)
|
||||||
|
|
||||||
|
if os.viewport != nil {
|
||||||
|
_ = os.wlSurface.SetBufferScale(1)
|
||||||
|
_ = os.viewport.SetSource(0, 0, float64(slot.shm.Width), float64(slot.shm.Height))
|
||||||
|
_ = os.viewport.SetDestination(int32(os.logicalW), int32(os.logicalH))
|
||||||
|
} else {
|
||||||
|
bufferScale := os.output.scale
|
||||||
|
if bufferScale <= 0 {
|
||||||
|
bufferScale = 1
|
||||||
|
}
|
||||||
|
_ = os.wlSurface.SetBufferScale(bufferScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = os.wlSurface.Attach(slot.wlBuf, 0, 0)
|
||||||
|
_ = os.wlSurface.Damage(0, 0, int32(os.logicalW), int32(os.logicalH))
|
||||||
|
_ = os.wlSurface.Commit()
|
||||||
|
|
||||||
|
// Mark this slot as busy until compositor releases it
|
||||||
|
slot.busy = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) cleanup() {
|
||||||
|
if r.cursorWlBuf != nil {
|
||||||
|
r.cursorWlBuf.Destroy()
|
||||||
|
}
|
||||||
|
if r.cursorPool != nil {
|
||||||
|
r.cursorPool.Destroy()
|
||||||
|
}
|
||||||
|
if r.cursorSurface != nil {
|
||||||
|
r.cursorSurface.Destroy()
|
||||||
|
}
|
||||||
|
if r.cursorBuffer != nil {
|
||||||
|
r.cursorBuffer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
for _, slot := range os.slots {
|
||||||
|
if slot == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slot.wlBuf != nil {
|
||||||
|
slot.wlBuf.Destroy()
|
||||||
|
}
|
||||||
|
if slot.pool != nil {
|
||||||
|
slot.pool.Destroy()
|
||||||
|
}
|
||||||
|
if slot.shm != nil {
|
||||||
|
slot.shm.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if os.viewport != nil {
|
||||||
|
os.viewport.Destroy()
|
||||||
|
}
|
||||||
|
if os.layerSurf != nil {
|
||||||
|
os.layerSurf.Destroy()
|
||||||
|
}
|
||||||
|
if os.wlSurface != nil {
|
||||||
|
os.wlSurface.Destroy()
|
||||||
|
}
|
||||||
|
if os.screenBuf != nil {
|
||||||
|
os.screenBuf.Close()
|
||||||
|
}
|
||||||
|
if os.screenBufNoCursor != nil {
|
||||||
|
os.screenBufNoCursor.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.shortcutsInhibitor != nil {
|
||||||
|
_ = r.shortcutsInhibitor.Destroy()
|
||||||
|
}
|
||||||
|
if r.shortcutsInhibitMgr != nil {
|
||||||
|
_ = r.shortcutsInhibitMgr.Destroy()
|
||||||
|
}
|
||||||
|
if r.viewporter != nil {
|
||||||
|
r.viewporter.Destroy()
|
||||||
|
}
|
||||||
|
if r.screencopy != nil {
|
||||||
|
r.screencopy.Destroy()
|
||||||
|
}
|
||||||
|
if r.pointer != nil {
|
||||||
|
r.pointer.Release()
|
||||||
|
}
|
||||||
|
if r.keyboard != nil {
|
||||||
|
r.keyboard.Release()
|
||||||
|
}
|
||||||
|
if r.display != nil {
|
||||||
|
r.ctx.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
271
core/internal/screenshot/region_input.go
Normal file
271
core/internal/screenshot/region_input.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package screenshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupInput() {
|
||||||
|
if r.seat == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && r.pointer == nil {
|
||||||
|
if pointer, err := r.seat.GetPointer(); err == nil {
|
||||||
|
r.pointer = pointer
|
||||||
|
r.setupPointerHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && r.keyboard == nil {
|
||||||
|
if keyboard, err := r.seat.GetKeyboard(); err == nil {
|
||||||
|
r.keyboard = keyboard
|
||||||
|
r.setupKeyboardHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupPointerHandlers() {
|
||||||
|
r.pointer.SetEnterHandler(func(e client.PointerEnterEvent) {
|
||||||
|
if r.cursorSurface != nil {
|
||||||
|
_ = r.pointer.SetCursor(e.Serial, r.cursorSurface, 12, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.activeSurface = nil
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
if os.wlSurface.ID() == e.Surface.ID() {
|
||||||
|
r.activeSurface = os
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.pointerX = e.SurfaceX
|
||||||
|
r.pointerY = e.SurfaceY
|
||||||
|
})
|
||||||
|
|
||||||
|
r.pointer.SetMotionHandler(func(e client.PointerMotionEvent) {
|
||||||
|
if r.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.pointerX = e.SurfaceX
|
||||||
|
r.pointerY = e.SurfaceY
|
||||||
|
|
||||||
|
if !r.selection.dragging {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
curX, curY := e.SurfaceX, e.SurfaceY
|
||||||
|
if r.shiftHeld {
|
||||||
|
dx := curX - r.selection.anchorX
|
||||||
|
dy := curY - r.selection.anchorY
|
||||||
|
adx, ady := dx, dy
|
||||||
|
if adx < 0 {
|
||||||
|
adx = -adx
|
||||||
|
}
|
||||||
|
if ady < 0 {
|
||||||
|
ady = -ady
|
||||||
|
}
|
||||||
|
size := adx
|
||||||
|
if ady > adx {
|
||||||
|
size = ady
|
||||||
|
}
|
||||||
|
if dx < 0 {
|
||||||
|
curX = r.selection.anchorX - size
|
||||||
|
} else {
|
||||||
|
curX = r.selection.anchorX + size
|
||||||
|
}
|
||||||
|
if dy < 0 {
|
||||||
|
curY = r.selection.anchorY - size
|
||||||
|
} else {
|
||||||
|
curY = r.selection.anchorY + size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.selection.currentX = curX
|
||||||
|
r.selection.currentY = curY
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
r.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||||
|
if r.activeSurface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Button {
|
||||||
|
case 0x110: // BTN_LEFT
|
||||||
|
switch e.State {
|
||||||
|
case 1: // pressed
|
||||||
|
r.preSelect = Region{}
|
||||||
|
r.selection.hasSelection = true
|
||||||
|
r.selection.dragging = true
|
||||||
|
r.selection.surface = r.activeSurface
|
||||||
|
r.selection.anchorX = r.pointerX
|
||||||
|
r.selection.anchorY = r.pointerY
|
||||||
|
r.selection.currentX = r.pointerX
|
||||||
|
r.selection.currentY = r.pointerY
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
case 0: // released
|
||||||
|
r.selection.dragging = false
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
r.cancelled = true
|
||||||
|
r.running = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) setupKeyboardHandlers() {
|
||||||
|
r.keyboard.SetModifiersHandler(func(e client.KeyboardModifiersEvent) {
|
||||||
|
r.shiftHeld = e.ModsDepressed&1 != 0
|
||||||
|
})
|
||||||
|
|
||||||
|
r.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||||
|
if e.State != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Key {
|
||||||
|
case 1:
|
||||||
|
r.cancelled = true
|
||||||
|
r.running = false
|
||||||
|
case 25:
|
||||||
|
r.showCapturedCursor = !r.showCapturedCursor
|
||||||
|
for _, os := range r.surfaces {
|
||||||
|
r.redrawSurface(os)
|
||||||
|
}
|
||||||
|
case 28, 57:
|
||||||
|
if r.selection.hasSelection {
|
||||||
|
r.finishSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegionSelector) finishSelection() {
|
||||||
|
if r.selection.surface == nil {
|
||||||
|
r.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
os := r.selection.surface
|
||||||
|
srcBuf := r.getSourceBuffer(os)
|
||||||
|
if srcBuf == nil {
|
||||||
|
r.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
x1, y1 := r.selection.anchorX, r.selection.anchorY
|
||||||
|
x2, y2 := r.selection.currentX, r.selection.currentY
|
||||||
|
|
||||||
|
if x1 > x2 {
|
||||||
|
x1, x2 = x2, x1
|
||||||
|
}
|
||||||
|
if y1 > y2 {
|
||||||
|
y1, y2 = y2, y1
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleX, scaleY := 1.0, 1.0
|
||||||
|
if os.logicalW > 0 {
|
||||||
|
scaleX = float64(srcBuf.Width) / float64(os.logicalW)
|
||||||
|
scaleY = float64(srcBuf.Height) / float64(os.logicalH)
|
||||||
|
}
|
||||||
|
|
||||||
|
bx1 := int(x1 * scaleX)
|
||||||
|
by1 := int(y1 * scaleY)
|
||||||
|
bx2 := int(x2 * scaleX)
|
||||||
|
by2 := int(y2 * scaleY)
|
||||||
|
|
||||||
|
// Clamp to buffer bounds
|
||||||
|
if bx1 < 0 {
|
||||||
|
bx1 = 0
|
||||||
|
}
|
||||||
|
if by1 < 0 {
|
||||||
|
by1 = 0
|
||||||
|
}
|
||||||
|
if bx2 > srcBuf.Width {
|
||||||
|
bx2 = srcBuf.Width
|
||||||
|
}
|
||||||
|
if by2 > srcBuf.Height {
|
||||||
|
by2 = srcBuf.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
w, h := bx2-bx1+1, by2-by1+1
|
||||||
|
if r.shiftHeld && w != h {
|
||||||
|
if w < h {
|
||||||
|
h = w
|
||||||
|
} else {
|
||||||
|
w = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w < 1 {
|
||||||
|
w = 1
|
||||||
|
}
|
||||||
|
if h < 1 {
|
||||||
|
h = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cropped buffer and copy pixels directly
|
||||||
|
cropped, err := CreateShmBuffer(w, h, w*4)
|
||||||
|
if err != nil {
|
||||||
|
r.running = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
srcData := srcBuf.Data()
|
||||||
|
dstData := cropped.Data()
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
srcY := by1 + y
|
||||||
|
if os.yInverted {
|
||||||
|
srcY = srcBuf.Height - 1 - (by1 + y)
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= srcBuf.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dstY := y
|
||||||
|
if os.yInverted {
|
||||||
|
dstY = h - 1 - y
|
||||||
|
}
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
srcX := bx1 + x
|
||||||
|
if srcX < 0 || srcX >= srcBuf.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
si := srcY*srcBuf.Stride + srcX*4
|
||||||
|
di := dstY*cropped.Stride + x*4
|
||||||
|
if si+3 < len(srcData) && di+3 < len(dstData) {
|
||||||
|
dstData[di+0] = srcData[si+0]
|
||||||
|
dstData[di+1] = srcData[si+1]
|
||||||
|
dstData[di+2] = srcData[si+2]
|
||||||
|
dstData[di+3] = srcData[si+3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.capturedBuffer = cropped
|
||||||
|
r.capturedRegion = Region{
|
||||||
|
X: int32(bx1),
|
||||||
|
Y: int32(by1),
|
||||||
|
Width: int32(w),
|
||||||
|
Height: int32(h),
|
||||||
|
Output: os.output.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also store for "last region" feature with global coords
|
||||||
|
r.result = Region{
|
||||||
|
X: int32(bx1) + os.output.x,
|
||||||
|
Y: int32(by1) + os.output.y,
|
||||||
|
Width: int32(w),
|
||||||
|
Height: int32(h),
|
||||||
|
Output: os.output.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.running = false
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
939
core/internal/screenshot/screenshot.go
Normal file
939
core/internal/screenshot/screenshot.go
Normal file
@@ -0,0 +1,939 @@
|
|||||||
|
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
|
||||||
|
fractionalScale float64
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
var output *WaylandOutput
|
||||||
|
if geom.Output != "" {
|
||||||
|
output = s.findOutputByName(geom.Output)
|
||||||
|
}
|
||||||
|
if output == nil {
|
||||||
|
output = s.findOutputForRegion(region)
|
||||||
|
}
|
||||||
|
if output == nil {
|
||||||
|
return nil, fmt.Errorf("could not find output for window")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch DetectCompositor() {
|
||||||
|
case CompositorHyprland:
|
||||||
|
return s.captureAndCrop(output, region)
|
||||||
|
case CompositorDWL:
|
||||||
|
return s.captureDWLWindow(output, region, geom.Scale)
|
||||||
|
default:
|
||||||
|
return s.captureRegionOnOutput(output, region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, dwlScale float64) (*CaptureResult, error) {
|
||||||
|
result, err := s.captureWholeOutput(output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := dwlScale
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = float64(result.Buffer.Width) / float64(output.width)
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
localX := int(float64(region.X) * scale)
|
||||||
|
localY := int(float64(region.Y) * scale)
|
||||||
|
if localX >= result.Buffer.Width {
|
||||||
|
localX = localX % result.Buffer.Width
|
||||||
|
}
|
||||||
|
if localY >= result.Buffer.Height {
|
||||||
|
localY = localY % result.Buffer.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
w := int(float64(region.Width) * scale)
|
||||||
|
h := int(float64(region.Height) * scale)
|
||||||
|
|
||||||
|
if localY+h > result.Buffer.Height && h <= result.Buffer.Height {
|
||||||
|
localY = result.Buffer.Height - h
|
||||||
|
if localY < 0 {
|
||||||
|
localY = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if localX+w > result.Buffer.Width && w <= result.Buffer.Width {
|
||||||
|
localX = result.Buffer.Width - w
|
||||||
|
if localX < 0 {
|
||||||
|
localX = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if localX < 0 {
|
||||||
|
w += localX
|
||||||
|
localX = 0
|
||||||
|
}
|
||||||
|
if localY < 0 {
|
||||||
|
h += localY
|
||||||
|
localY = 0
|
||||||
|
}
|
||||||
|
if localX+w > result.Buffer.Width {
|
||||||
|
w = result.Buffer.Width - localX
|
||||||
|
}
|
||||||
|
if localY+h > result.Buffer.Height {
|
||||||
|
h = result.Buffer.Height - localY
|
||||||
|
}
|
||||||
|
|
||||||
|
if w <= 0 || h <= 0 {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("window not visible on output")
|
||||||
|
}
|
||||||
|
|
||||||
|
cropped, err := CreateShmBuffer(w, h, w*4)
|
||||||
|
if err != nil {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("create crop buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcData := result.Buffer.Data()
|
||||||
|
dstData := cropped.Data()
|
||||||
|
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
srcY := localY + y
|
||||||
|
if result.YInverted {
|
||||||
|
srcY = result.Buffer.Height - 1 - (localY + y)
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= result.Buffer.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dstY := y
|
||||||
|
if result.YInverted {
|
||||||
|
dstY = h - 1 - y
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
srcX := localX + x
|
||||||
|
if srcX < 0 || srcX >= result.Buffer.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
si := srcY*result.Buffer.Stride + srcX*4
|
||||||
|
di := dstY*cropped.Stride + x*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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Buffer.Close()
|
||||||
|
cropped.Format = PixelFormat(result.Format)
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: cropped,
|
||||||
|
Region: region,
|
||||||
|
YInverted: false,
|
||||||
|
Format: result.Format,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
outputs = append(outputs, o)
|
||||||
|
}
|
||||||
|
s.outputsMu.Unlock()
|
||||||
|
|
||||||
|
if len(outputs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no outputs available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outputs) == 1 {
|
||||||
|
return s.captureWholeOutput(outputs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture all outputs first to get actual buffer sizes
|
||||||
|
type capturedOutput struct {
|
||||||
|
output *WaylandOutput
|
||||||
|
result *CaptureResult
|
||||||
|
physX int
|
||||||
|
physY int
|
||||||
|
}
|
||||||
|
captured := make([]capturedOutput, 0, len(outputs))
|
||||||
|
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
first := true
|
||||||
|
|
||||||
|
for _, output := range outputs {
|
||||||
|
result, err := s.captureWholeOutput(output)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("failed to capture output", "name", output.name, "err", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outX, outY := output.x, output.y
|
||||||
|
scale := float64(output.scale)
|
||||||
|
if DetectCompositor() == CompositorHyprland {
|
||||||
|
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||||
|
outX, outY = hx, hy
|
||||||
|
}
|
||||||
|
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
||||||
|
scale = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
physX := int(float64(outX) * scale)
|
||||||
|
physY := int(float64(outY) * scale)
|
||||||
|
|
||||||
|
captured = append(captured, capturedOutput{
|
||||||
|
output: output,
|
||||||
|
result: result,
|
||||||
|
physX: physX,
|
||||||
|
physY: physY,
|
||||||
|
})
|
||||||
|
|
||||||
|
right := physX + result.Buffer.Width
|
||||||
|
bottom := physY + result.Buffer.Height
|
||||||
|
|
||||||
|
if first {
|
||||||
|
minX, minY = physX, physY
|
||||||
|
maxX, maxY = right, bottom
|
||||||
|
first = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if physX < minX {
|
||||||
|
minX = physX
|
||||||
|
}
|
||||||
|
if physY < minY {
|
||||||
|
minY = physY
|
||||||
|
}
|
||||||
|
if right > maxX {
|
||||||
|
maxX = right
|
||||||
|
}
|
||||||
|
if bottom > maxY {
|
||||||
|
maxY = bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(captured) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to capture any outputs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(captured) == 1 {
|
||||||
|
return captured[0].result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalW := maxX - minX
|
||||||
|
totalH := maxY - minY
|
||||||
|
|
||||||
|
compositeStride := totalW * 4
|
||||||
|
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
||||||
|
if err != nil {
|
||||||
|
for _, c := range captured {
|
||||||
|
c.result.Buffer.Close()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
composite.Clear()
|
||||||
|
|
||||||
|
var format uint32
|
||||||
|
for _, c := range captured {
|
||||||
|
if format == 0 {
|
||||||
|
format = c.result.Format
|
||||||
|
}
|
||||||
|
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
||||||
|
c.result.Buffer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: composite,
|
||||||
|
Region: Region{X: int32(minX), Y: int32(minY), Width: int32(totalW), Height: int32(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) captureAndCrop(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||||
|
result, err := s.captureWholeOutput(output)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outX, outY := output.x, output.y
|
||||||
|
scale := float64(output.scale)
|
||||||
|
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||||
|
outX, outY = hx, hy
|
||||||
|
}
|
||||||
|
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
||||||
|
scale = s
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
localX := int(float64(region.X-outX) * scale)
|
||||||
|
localY := int(float64(region.Y-outY) * scale)
|
||||||
|
w := int(float64(region.Width) * scale)
|
||||||
|
h := int(float64(region.Height) * scale)
|
||||||
|
|
||||||
|
cropped, err := CreateShmBuffer(w, h, w*4)
|
||||||
|
if err != nil {
|
||||||
|
result.Buffer.Close()
|
||||||
|
return nil, fmt.Errorf("create crop buffer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcData := result.Buffer.Data()
|
||||||
|
dstData := cropped.Data()
|
||||||
|
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
srcY := localY + y
|
||||||
|
if result.YInverted {
|
||||||
|
srcY = result.Buffer.Height - 1 - (localY + y)
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= result.Buffer.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dstY := y
|
||||||
|
if result.YInverted {
|
||||||
|
dstY = h - 1 - y
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
srcX := localX + x
|
||||||
|
if srcX < 0 || srcX >= result.Buffer.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
si := srcY*result.Buffer.Stride + srcX*4
|
||||||
|
di := dstY*cropped.Stride + x*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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Buffer.Close()
|
||||||
|
cropped.Format = PixelFormat(result.Format)
|
||||||
|
|
||||||
|
return &CaptureResult{
|
||||||
|
Buffer: cropped,
|
||||||
|
Region: region,
|
||||||
|
YInverted: false,
|
||||||
|
Format: result.Format,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
|
||||||
|
scale := output.fractionalScale
|
||||||
|
if scale <= 0 && DetectCompositor() == CompositorHyprland {
|
||||||
|
scale = GetHyprlandMonitorScale(output.name)
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = float64(output.scale)
|
||||||
|
}
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
localX := int32(float64(region.X-output.x) * scale)
|
||||||
|
localY := int32(float64(region.Y-output.y) * scale)
|
||||||
|
w := int32(float64(region.Width) * scale)
|
||||||
|
h := int32(float64(region.Height) * scale)
|
||||||
|
|
||||||
|
if DetectCompositor() == CompositorDWL {
|
||||||
|
scaledOutW := int32(float64(output.width) * scale)
|
||||||
|
scaledOutH := int32(float64(output.height) * scale)
|
||||||
|
if localX >= scaledOutW {
|
||||||
|
localX = localX % scaledOutW
|
||||||
|
}
|
||||||
|
if localY >= scaledOutH {
|
||||||
|
localY = localY % scaledOutH
|
||||||
|
}
|
||||||
|
if localX+w > scaledOutW {
|
||||||
|
w = scaledOutW - localX
|
||||||
|
}
|
||||||
|
if localY+h > scaledOutH {
|
||||||
|
h = scaledOutH - localY
|
||||||
|
}
|
||||||
|
if localX < 0 {
|
||||||
|
w += localX
|
||||||
|
localX = 0
|
||||||
|
}
|
||||||
|
if localY < 0 {
|
||||||
|
h += localY
|
||||||
|
localY = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := int32(0)
|
||||||
|
if s.config.IncludeCursor {
|
||||||
|
cursor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
|
||||||
|
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 pool *client.ShmPool
|
||||||
|
var wlBuf *client.Buffer
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
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()
|
||||||
|
pool = nil
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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 wlBuf != nil {
|
||||||
|
wlBuf.Destroy()
|
||||||
|
}
|
||||||
|
if pool != nil {
|
||||||
|
pool.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) findOutputByName(name string) *WaylandOutput {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
defer s.outputsMu.Unlock()
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
if o.name == name {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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 {
|
||||||
|
x, y, w, h := o.x, o.y, o.width, o.height
|
||||||
|
if DetectCompositor() == CompositorHyprland {
|
||||||
|
if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
|
||||||
|
x, y, w, h = hx, hy, hw, hh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cx >= x && cx < x+w && cy >= y && cy < y+h {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
x, y, w, h := o.x, o.y, o.width, o.height
|
||||||
|
if DetectCompositor() == CompositorHyprland {
|
||||||
|
if hx, hy, hw, hh, ok := GetHyprlandMonitorGeometry(o.name); ok {
|
||||||
|
x, y, w, h = hx, hy, hw, hh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if region.X >= x && region.X < x+w &&
|
||||||
|
region.Y >= y && region.Y < y+h {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Screenshoter) findFocusedOutput() *WaylandOutput {
|
||||||
|
if mon := GetFocusedMonitor(); mon != "" {
|
||||||
|
s.outputsMu.Lock()
|
||||||
|
defer s.outputsMu.Unlock()
|
||||||
|
for _, o := range s.outputs {
|
||||||
|
if o.name == mon {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
fractionalScale: 1.0,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
o.fractionalScale = float64(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)
|
return b.scanI2CDevicesInternal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DDCBackend) ForceRescan() error {
|
||||||
|
return b.scanI2CDevicesInternal(true)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
||||||
b.scanMutex.Lock()
|
b.scanMutex.Lock()
|
||||||
defer b.scanMutex.Unlock()
|
defer b.scanMutex.Unlock()
|
||||||
@@ -64,10 +68,6 @@ func (b *DDCBackend) scanI2CDevicesInternal(force bool) error {
|
|||||||
activeBuses[i] = true
|
activeBuses[i] = true
|
||||||
id := fmt.Sprintf("ddc:i2c-%d", i)
|
id := fmt.Sprintf("ddc:i2c-%d", i)
|
||||||
|
|
||||||
if _, exists := b.devices.Load(id); exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dev, err := b.probeDDCDevice(i)
|
dev, err := b.probeDDCDevice(i)
|
||||||
if err != nil || dev == nil {
|
if err != nil || dev == nil {
|
||||||
continue
|
continue
|
||||||
@@ -261,8 +261,16 @@ func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int) er
|
|||||||
|
|
||||||
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
||||||
|
|
||||||
|
if _, err := os.Stat(busPath); os.IsNotExist(err) {
|
||||||
|
b.devices.Delete(id)
|
||||||
|
log.Debugf("removed stale DDC device %s (bus no longer exists)", id)
|
||||||
|
return fmt.Errorf("device disconnected: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
b.devices.Delete(id)
|
||||||
|
log.Debugf("removed DDC device %s (open failed: %v)", id, err)
|
||||||
return fmt.Errorf("open i2c device: %w", err)
|
return fmt.Errorf("open i2c device: %w", err)
|
||||||
}
|
}
|
||||||
defer syscall.Close(fd)
|
defer syscall.Close(fd)
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ func (m *Manager) initDDC() {
|
|||||||
|
|
||||||
func (m *Manager) Rescan() {
|
func (m *Manager) Rescan() {
|
||||||
log.Debug("Rescanning brightness devices...")
|
log.Debug("Rescanning brightness devices...")
|
||||||
|
|
||||||
|
if m.ddcReady && m.ddcBackend != nil {
|
||||||
|
if err := m.ddcBackend.ForceRescan(); err != nil {
|
||||||
|
log.Debugf("DDC force rescan failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/pilebones/go-udev/netlink"
|
"github.com/pilebones/go-udev/netlink"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UdevMonitor struct {
|
type UdevMonitor struct {
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
|
rescanMutex sync.Mutex
|
||||||
|
rescanTimer *time.Timer
|
||||||
|
rescanPending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
func NewUdevMonitor(manager *Manager) *UdevMonitor {
|
||||||
@@ -34,10 +39,8 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
matcher := &netlink.RuleDefinitions{
|
matcher := &netlink.RuleDefinitions{
|
||||||
Rules: []netlink.RuleDefinition{
|
Rules: []netlink.RuleDefinition{
|
||||||
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
{Env: map[string]string{"SUBSYSTEM": "backlight"}},
|
||||||
// ! TODO: most drivers dont emit this for leds?
|
{Env: map[string]string{"SUBSYSTEM": "drm"}},
|
||||||
// ! inotify brightness_hw_changed works, but thn some devices dont do that...
|
{Env: map[string]string{"SUBSYSTEM": "i2c"}},
|
||||||
// ! So for now the GUI just shows OSDs for leds, without reflecting actual HW value
|
|
||||||
// {Env: map[string]string{"SUBSYSTEM": "leds"}},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := matcher.Compile(); err != nil {
|
if err := matcher.Compile(); err != nil {
|
||||||
@@ -49,7 +52,7 @@ func (m *UdevMonitor) run(manager *Manager) {
|
|||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
conn.Monitor(events, errs, matcher)
|
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 {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -75,11 +78,54 @@ func (m *UdevMonitor) handleEvent(manager *Manager, event netlink.UEvent) {
|
|||||||
sysname := filepath.Base(devpath)
|
sysname := filepath.Base(devpath)
|
||||||
action := string(event.Action)
|
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 {
|
switch action {
|
||||||
case "change":
|
case "change":
|
||||||
m.handleChange(manager, subsystem, sysname)
|
m.handleChange(manager, "backlight", sysname)
|
||||||
case "add", "remove":
|
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()
|
manager.Rescan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
91
core/internal/server/matugen_handler.go
Normal file
91
core/internal/server/matugen_handler.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MatugenQueueResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMatugenQueue(conn net.Conn, req models.Request) {
|
||||||
|
getString := func(key string) string {
|
||||||
|
if v, ok := req.Params[key].(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
getBool := func(key string, def bool) bool {
|
||||||
|
if v, ok := req.Params[key].(bool); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := matugen.Options{
|
||||||
|
StateDir: getString("stateDir"),
|
||||||
|
ShellDir: getString("shellDir"),
|
||||||
|
ConfigDir: getString("configDir"),
|
||||||
|
Kind: getString("kind"),
|
||||||
|
Value: getString("value"),
|
||||||
|
Mode: getString("mode"),
|
||||||
|
IconTheme: getString("iconTheme"),
|
||||||
|
MatugenType: getString("matugenType"),
|
||||||
|
RunUserTemplates: getBool("runUserTemplates", true),
|
||||||
|
StockColors: getString("stockColors"),
|
||||||
|
SyncModeWithPortal: getBool("syncModeWithPortal", false),
|
||||||
|
TerminalsAlwaysDark: getBool("terminalsAlwaysDark", false),
|
||||||
|
}
|
||||||
|
|
||||||
|
wait := getBool("wait", true)
|
||||||
|
|
||||||
|
queue := matugen.GetQueue()
|
||||||
|
resultCh := queue.Submit(opts)
|
||||||
|
|
||||||
|
if !wait {
|
||||||
|
models.Respond(conn, req.ID, MatugenQueueResult{
|
||||||
|
Success: true,
|
||||||
|
Message: "queued",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultCh:
|
||||||
|
if result.Error != nil {
|
||||||
|
if result.Error == context.Canceled {
|
||||||
|
models.Respond(conn, req.ID, MatugenQueueResult{
|
||||||
|
Success: false,
|
||||||
|
Message: "cancelled",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
models.RespondError(conn, req.ID, result.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
models.Respond(conn, req.ID, MatugenQueueResult{
|
||||||
|
Success: true,
|
||||||
|
Message: "completed",
|
||||||
|
})
|
||||||
|
case <-ctx.Done():
|
||||||
|
models.RespondError(conn, req.ID, "timeout waiting for theme generation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMatugenStatus(conn net.Conn, req models.Request) {
|
||||||
|
queue := matugen.GetQueue()
|
||||||
|
models.Respond(conn, req.ID, map[string]bool{
|
||||||
|
"running": queue.IsRunning(),
|
||||||
|
"pending": queue.HasPending(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -215,6 +215,10 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
models.Respond(conn, req.ID, info)
|
models.Respond(conn, req.ID, info)
|
||||||
case "subscribe":
|
case "subscribe":
|
||||||
handleSubscribe(conn, req)
|
handleSubscribe(conn, req)
|
||||||
|
case "matugen.queue":
|
||||||
|
handleMatugenQueue(conn, req)
|
||||||
|
case "matugen.status":
|
||||||
|
handleMatugenStatus(conn, req)
|
||||||
default:
|
default:
|
||||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@@ -113,7 +113,7 @@ func (i *Display) GetRegistry() (*Registry, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Display) Destroy() error {
|
func (i *Display) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,15 +224,16 @@ func (i *Display) Dispatch(opcode uint32, fd int, data []byte) {
|
|||||||
|
|
||||||
i.errorHandler(e)
|
i.errorHandler(e)
|
||||||
case 1:
|
case 1:
|
||||||
if i.deleteIdHandler == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var e DisplayDeleteIdEvent
|
var e DisplayDeleteIdEvent
|
||||||
l := 0
|
l := 0
|
||||||
e.Id = Uint32(data[l : l+4])
|
e.Id = Uint32(data[l : l+4])
|
||||||
l += 4
|
l += 4
|
||||||
|
|
||||||
i.deleteIdHandler(e)
|
i.Context().DeleteID(e.Id)
|
||||||
|
|
||||||
|
if i.deleteIdHandler != nil {
|
||||||
|
i.deleteIdHandler(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +327,7 @@ func (i *Registry) Bind(name uint32, iface string, version uint32, id Proxy) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Registry) Destroy() error {
|
func (i *Registry) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +434,7 @@ func NewCallback(ctx *Context) *Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Callback) Destroy() error {
|
func (i *Callback) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +530,7 @@ func (i *Compositor) CreateRegion() (*Region, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Compositor) Destroy() error {
|
func (i *Compositor) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,7 +620,7 @@ func (i *ShmPool) CreateBuffer(offset, width, height, stride int32, format uint3
|
|||||||
// buffers that have been created from this pool
|
// buffers that have been created from this pool
|
||||||
// are gone.
|
// are gone.
|
||||||
func (i *ShmPool) Destroy() error {
|
func (i *ShmPool) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -735,7 +736,7 @@ func (i *Shm) CreatePool(fd int, size int32) (*ShmPool, error) {
|
|||||||
//
|
//
|
||||||
// Objects created via this interface remain unaffected.
|
// Objects created via this interface remain unaffected.
|
||||||
func (i *Shm) Release() error {
|
func (i *Shm) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -1642,7 +1643,7 @@ func NewBuffer(ctx *Context) *Buffer {
|
|||||||
//
|
//
|
||||||
// For possible side-effects to a surface, see wl_surface.attach.
|
// For possible side-effects to a surface, see wl_surface.attach.
|
||||||
func (i *Buffer) Destroy() error {
|
func (i *Buffer) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -1803,7 +1804,7 @@ func (i *DataOffer) Receive(mimeType string, fd int) error {
|
|||||||
//
|
//
|
||||||
// Destroy the data offer.
|
// Destroy the data offer.
|
||||||
func (i *DataOffer) Destroy() error {
|
func (i *DataOffer) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 2
|
const opcode = 2
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -2120,7 +2121,7 @@ func (i *DataSource) Offer(mimeType string) error {
|
|||||||
//
|
//
|
||||||
// Destroy the data source.
|
// Destroy the data source.
|
||||||
func (i *DataSource) Destroy() error {
|
func (i *DataSource) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -2540,7 +2541,7 @@ func (i *DataDevice) SetSelection(source *DataSource, serial uint32) error {
|
|||||||
//
|
//
|
||||||
// This request destroys the data device.
|
// This request destroys the data device.
|
||||||
func (i *DataDevice) Release() error {
|
func (i *DataDevice) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 2
|
const opcode = 2
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -2859,7 +2860,7 @@ func (i *DataDeviceManager) GetDataDevice(seat *Seat) (*DataDevice, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *DataDeviceManager) Destroy() error {
|
func (i *DataDeviceManager) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3000,7 +3001,7 @@ func (i *Shell) GetShellSurface(surface *Surface) (*ShellSurface, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Shell) Destroy() error {
|
func (i *Shell) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3421,7 +3422,7 @@ func (i *ShellSurface) SetClass(class string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ShellSurface) Destroy() error {
|
func (i *ShellSurface) Destroy() error {
|
||||||
i.Context().Unregister(i)
|
i.MarkZombie()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3798,7 +3799,7 @@ func NewSurface(ctx *Context) *Surface {
|
|||||||
//
|
//
|
||||||
// Deletes the surface and invalidates its object ID.
|
// Deletes the surface and invalidates its object ID.
|
||||||
func (i *Surface) Destroy() error {
|
func (i *Surface) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -4618,7 +4619,7 @@ func (i *Seat) GetTouch() (*Touch, error) {
|
|||||||
// Using this request a client can tell the server that it is not going to
|
// Using this request a client can tell the server that it is not going to
|
||||||
// use the seat object anymore.
|
// use the seat object anymore.
|
||||||
func (i *Seat) Release() error {
|
func (i *Seat) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 3
|
const opcode = 3
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -4920,7 +4921,7 @@ func (i *Pointer) SetCursor(serial uint32, surface *Surface, hotspotX, hotspotY
|
|||||||
// This request destroys the pointer proxy object, so clients must not call
|
// This request destroys the pointer proxy object, so clients must not call
|
||||||
// wl_pointer_destroy() after using this request.
|
// wl_pointer_destroy() after using this request.
|
||||||
func (i *Pointer) Release() error {
|
func (i *Pointer) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 1
|
const opcode = 1
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -5685,7 +5686,7 @@ func NewKeyboard(ctx *Context) *Keyboard {
|
|||||||
|
|
||||||
// Release : release the keyboard object
|
// Release : release the keyboard object
|
||||||
func (i *Keyboard) Release() error {
|
func (i *Keyboard) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -6091,7 +6092,7 @@ func NewTouch(ctx *Context) *Touch {
|
|||||||
|
|
||||||
// Release : release the touch object
|
// Release : release the touch object
|
||||||
func (i *Touch) Release() error {
|
func (i *Touch) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -6406,7 +6407,7 @@ func NewOutput(ctx *Context) *Output {
|
|||||||
// Using this request a client can tell the server that it is not going to
|
// Using this request a client can tell the server that it is not going to
|
||||||
// use the output object anymore.
|
// use the output object anymore.
|
||||||
func (i *Output) Release() error {
|
func (i *Output) Release() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -6923,7 +6924,7 @@ func NewRegion(ctx *Context) *Region {
|
|||||||
//
|
//
|
||||||
// Destroy the region. This will invalidate the object ID.
|
// Destroy the region. This will invalidate the object ID.
|
||||||
func (i *Region) Destroy() error {
|
func (i *Region) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -7057,7 +7058,7 @@ func NewSubcompositor(ctx *Context) *Subcompositor {
|
|||||||
// protocol object anymore. This does not affect any other
|
// protocol object anymore. This does not affect any other
|
||||||
// objects, wl_subsurface objects included.
|
// objects, wl_subsurface objects included.
|
||||||
func (i *Subcompositor) Destroy() error {
|
func (i *Subcompositor) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -7280,7 +7281,7 @@ func NewSubsurface(ctx *Context) *Subsurface {
|
|||||||
// wl_subcompositor.get_subsurface request. The wl_surface's association
|
// wl_subcompositor.get_subsurface request. The wl_surface's association
|
||||||
// to the parent is deleted. The wl_surface is unmapped immediately.
|
// to the parent is deleted. The wl_surface is unmapped immediately.
|
||||||
func (i *Subsurface) Destroy() error {
|
func (i *Subsurface) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
@@ -7499,7 +7500,7 @@ func NewFixes(ctx *Context) *Fixes {
|
|||||||
|
|
||||||
// Destroy : destroys this object
|
// Destroy : destroys this object
|
||||||
func (i *Fixes) Destroy() error {
|
func (i *Fixes) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 0
|
const opcode = 0
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
|
import "sync/atomic"
|
||||||
|
|
||||||
type Dispatcher interface {
|
type Dispatcher interface {
|
||||||
Dispatch(opcode uint32, fd int, data []byte)
|
Dispatch(opcode uint32, fd int, data []byte)
|
||||||
}
|
}
|
||||||
@@ -9,11 +11,14 @@ type Proxy interface {
|
|||||||
SetContext(ctx *Context)
|
SetContext(ctx *Context)
|
||||||
ID() uint32
|
ID() uint32
|
||||||
SetID(id uint32)
|
SetID(id uint32)
|
||||||
|
IsZombie() bool
|
||||||
|
MarkZombie()
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseProxy struct {
|
type BaseProxy struct {
|
||||||
ctx *Context
|
ctx *Context
|
||||||
id uint32
|
id uint32
|
||||||
|
zombie atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *BaseProxy) ID() uint32 {
|
func (p *BaseProxy) ID() uint32 {
|
||||||
@@ -31,3 +36,11 @@ func (p *BaseProxy) Context() *Context {
|
|||||||
func (p *BaseProxy) SetContext(ctx *Context) {
|
func (p *BaseProxy) SetContext(ctx *Context) {
|
||||||
p.ctx = ctx
|
p.ctx = ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *BaseProxy) IsZombie() bool {
|
||||||
|
return p.zombie.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BaseProxy) MarkZombie() {
|
||||||
|
p.zombie.Store(true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ func (ctx *Context) Unregister(p Proxy) {
|
|||||||
ctx.objects.Delete(p.ID())
|
ctx.objects.Delete(p.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) DeleteID(id uint32) {
|
||||||
|
ctx.objects.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx *Context) GetProxy(id uint32) Proxy {
|
func (ctx *Context) GetProxy(id uint32) Proxy {
|
||||||
if val, ok := ctx.objects.Load(id); ok {
|
if val, ok := ctx.objects.Load(id); ok {
|
||||||
return val
|
return val
|
||||||
@@ -72,7 +76,11 @@ func (ctx *Context) GetDispatch() func() error {
|
|||||||
return func() error {
|
return func() error {
|
||||||
proxy, ok := ctx.objects.Load(senderID)
|
proxy, ok := ctx.objects.Load(senderID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderNotFound, senderID)
|
return nil // Proxy already deleted via delete_id, silently ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if proxy.IsZombie() {
|
||||||
|
return nil // Zombie proxy, discard late events
|
||||||
}
|
}
|
||||||
|
|
||||||
sender, ok := proxy.(Dispatcher)
|
sender, ok := proxy.(Dispatcher)
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -33,7 +33,6 @@ Requires: dgop
|
|||||||
Recommends: cava
|
Recommends: cava
|
||||||
Recommends: cliphist
|
Recommends: cliphist
|
||||||
Recommends: danksearch
|
Recommends: danksearch
|
||||||
Recommends: hyprpicker
|
|
||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: quickshell-git
|
Recommends: quickshell-git
|
||||||
Recommends: wl-clipboard
|
Recommends: wl-clipboard
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
}: let
|
}: let
|
||||||
cfg = config.programs.dankMaterialShell;
|
cfg = config.programs.dankMaterialShell;
|
||||||
in {
|
in {
|
||||||
qmlPath = "${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms";
|
qmlPath = "${dmsPkgs.dms-shell}/share/quickshell/dms";
|
||||||
|
|
||||||
packages =
|
packages =
|
||||||
[
|
[
|
||||||
@@ -19,7 +19,7 @@ in {
|
|||||||
pkgs.libsForQt5.qt5ct
|
pkgs.libsForQt5.qt5ct
|
||||||
pkgs.kdePackages.qt6ct
|
pkgs.kdePackages.qt6ct
|
||||||
|
|
||||||
dmsPkgs.dmsCli
|
dmsPkgs.dms-shell
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
|
++ lib.optional cfg.enableSystemMonitoring dmsPkgs.dgop
|
||||||
++ lib.optionals cfg.enableClipboard [pkgs.cliphist pkgs.wl-clipboard]
|
++ lib.optionals cfg.enableClipboard [pkgs.cliphist pkgs.wl-clipboard]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"--command"
|
"--command"
|
||||||
cfg.compositor.name
|
cfg.compositor.name
|
||||||
"-p"
|
"-p"
|
||||||
"${dmsPkgs.dankMaterialShell}/etc/xdg/quickshell/dms"
|
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
||||||
]
|
]
|
||||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||||
"-C"
|
"-C"
|
||||||
@@ -61,7 +61,9 @@ in {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
quickshell = {
|
quickshell = {
|
||||||
package = lib.mkPackageOption pkgs "quickshell" {};
|
package = lib.mkPackageOption dmsPkgs "quickshell" {
|
||||||
|
extraDescription = "The quickshell package to use (defaults to be built from source, in the commit 26531f due to unreleased features used by DMS).";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
logs.save = lib.mkEnableOption "saving logs from DMS greeter to file";
|
logs.save = lib.mkEnableOption "saving logs from DMS greeter to file";
|
||||||
logs.path = lib.mkOption {
|
logs.path = lib.mkOption {
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
lib,
|
lib,
|
||||||
dmsPkgs,
|
dmsPkgs,
|
||||||
...
|
...
|
||||||
}: let
|
} @ args: let
|
||||||
cfg = config.programs.dankMaterialShell;
|
cfg = config.programs.dankMaterialShell;
|
||||||
jsonFormat = pkgs.formats.json {};
|
jsonFormat = pkgs.formats.json {};
|
||||||
common = import ./common.nix {inherit config pkgs lib dmsPkgs;};
|
common = import ./common.nix {inherit config pkgs lib dmsPkgs;};
|
||||||
in {
|
in {
|
||||||
imports = [
|
imports = [
|
||||||
./options.nix
|
(import ./options.nix args)
|
||||||
(lib.mkRemovedOptionModule ["programs" "dankMaterialShell" "enableNightMode"] "Night mode is now always available.")
|
(lib.mkRemovedOptionModule ["programs" "dankMaterialShell" "enableNightMode"] "Night mode is now always available.")
|
||||||
(lib.mkRenamedOptionModule ["programs" "dankMaterialShell" "enableSystemd"] ["programs" "dankMaterialShell" "systemd" "enable"])
|
(lib.mkRenamedOptionModule ["programs" "dankMaterialShell" "enableSystemd"] ["programs" "dankMaterialShell" "systemd" "enable"])
|
||||||
];
|
];
|
||||||
@@ -66,7 +66,7 @@ in {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
|
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +89,6 @@ in {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
home.packages = common.packages ++ [dmsPkgs.dankMaterialShell];
|
home.packages = common.packages;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
lib,
|
lib,
|
||||||
dmsPkgs,
|
dmsPkgs,
|
||||||
...
|
...
|
||||||
}: let
|
} @ args: let
|
||||||
cfg = config.programs.dankMaterialShell;
|
cfg = config.programs.dankMaterialShell;
|
||||||
common = import ./common.nix {inherit config pkgs lib dmsPkgs;};
|
common = import ./common.nix {inherit config pkgs lib dmsPkgs;};
|
||||||
in {
|
in {
|
||||||
imports = [
|
imports = [
|
||||||
./options.nix
|
(import ./options.nix args)
|
||||||
];
|
];
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable
|
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 {
|
systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
|
||||||
description = "DankMaterialShell";
|
description = "DankMaterialShell";
|
||||||
@@ -26,11 +26,11 @@ in {
|
|||||||
restartTriggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
|
restartTriggers = lib.optional cfg.systemd.restartIfChanged common.qmlPath;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dmsCli + " run --session";
|
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.systemPackages = [cfg.quickshell.package dmsPkgs.dankMaterialShell] ++ common.packages;
|
environment.systemPackages = [cfg.quickshell.package] ++ common.packages;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
pkgs,
|
|
||||||
lib,
|
lib,
|
||||||
|
dmsPkgs,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
@@ -62,7 +62,9 @@ in {
|
|||||||
description = "Add needed dependencies to have system sound support";
|
description = "Add needed dependencies to have system sound support";
|
||||||
};
|
};
|
||||||
quickshell = {
|
quickshell = {
|
||||||
package = lib.mkPackageOption pkgs "quickshell" {};
|
package = lib.mkPackageOption dmsPkgs "quickshell" {
|
||||||
|
extraDescription = "The quickshell package to use (defaults to be built from source, in the commit 26531f due to unreleased features used by DMS).";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,20 @@ esac
|
|||||||
OBS_PROJECT="${OBS_BASE_PROJECT}:${PROJECT}"
|
OBS_PROJECT="${OBS_BASE_PROJECT}:${PROJECT}"
|
||||||
|
|
||||||
echo "==> Target: $OBS_PROJECT / $PACKAGE"
|
echo "==> Target: $OBS_PROJECT / $PACKAGE"
|
||||||
|
|
||||||
|
# Detect if this is a manual run or automated
|
||||||
|
IS_MANUAL=false
|
||||||
|
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||||
|
IS_MANUAL=true
|
||||||
|
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
|
||||||
|
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then
|
||||||
|
IS_MANUAL=true
|
||||||
|
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)"
|
||||||
|
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
|
||||||
|
IS_MANUAL=true
|
||||||
|
echo "==> Local/manual run detected (not in CI)"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$UPLOAD_DEBIAN" == true && "$UPLOAD_OPENSUSE" == true ]]; then
|
if [[ "$UPLOAD_DEBIAN" == true && "$UPLOAD_OPENSUSE" == true ]]; then
|
||||||
echo "==> Distributions: Debian + OpenSUSE"
|
echo "==> Distributions: Debian + OpenSUSE"
|
||||||
elif [[ "$UPLOAD_DEBIAN" == true ]]; then
|
elif [[ "$UPLOAD_DEBIAN" == true ]]; then
|
||||||
@@ -192,9 +206,20 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
|||||||
if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then
|
||||||
if [[ "$OLD_RELEASE" =~ ^([0-9]+) ]]; then
|
if [[ "$OLD_RELEASE" =~ ^([0-9]+) ]]; then
|
||||||
BASE_RELEASE="${BASH_REMATCH[1]}"
|
BASE_RELEASE="${BASH_REMATCH[1]}"
|
||||||
NEXT_RELEASE=$((BASE_RELEASE + 1))
|
if [[ "$IS_MANUAL" == true ]]; then
|
||||||
echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)"
|
NEXT_RELEASE=$((BASE_RELEASE + 1))
|
||||||
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${NEXT_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)"
|
||||||
|
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${NEXT_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||||
|
else
|
||||||
|
echo " - Detected same version $NEW_VERSION (release $OLD_RELEASE). Not a manual run, skipping update."
|
||||||
|
# For automated runs with no version change, we should stop here to avoid unnecessary rebuilds
|
||||||
|
# However, we need to check if we are also updating Debian, or if this script is expected to continue.
|
||||||
|
# If this is OpenSUSE only run, we can exit.
|
||||||
|
if [[ "$UPLOAD_DEBIAN" == false ]]; then
|
||||||
|
echo "✅ No changes needed for OpenSUSE (not manual). Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)"
|
echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)"
|
||||||
@@ -643,175 +668,182 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[
|
|||||||
CHANGELOG_BASE=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
|
CHANGELOG_BASE=$(echo "$CHANGELOG_VERSION" | sed 's/ppa[0-9]*$//')
|
||||||
OLD_DSC_BASE=$(echo "$OLD_DSC_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
|
if [[ -n "$OLD_DSC_VERSION" ]] && [[ "$OLD_DSC_BASE" == "$CHANGELOG_BASE" ]]; then
|
||||||
echo "==> Detected rebuild of same base version $CHANGELOG_BASE, incrementing version"
|
if [[ "$IS_MANUAL" == true ]]; then
|
||||||
|
echo "==> Detected rebuild of same base version $CHANGELOG_BASE, incrementing version"
|
||||||
|
|
||||||
if [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git$ ]]; then
|
if [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git$ ]]; then
|
||||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||||
NEW_VERSION="${BASE_VERSION}+gitppa1"
|
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]}"
|
|
||||||
NEW_PPA_NUM=$((PPA_NUM + 1))
|
|
||||||
NEW_VERSION="${BASE_VERSION}ppa${NEW_PPA_NUM}"
|
|
||||||
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
|
||||||
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git([0-9]+)(\.[a-f0-9]+)?(ppa([0-9]+))?$ ]]; then
|
|
||||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
|
||||||
GIT_NUM="${BASH_REMATCH[2]}"
|
|
||||||
GIT_HASH="${BASH_REMATCH[3]}"
|
|
||||||
PPA_NUM="${BASH_REMATCH[5]}"
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
else
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa1"
|
|
||||||
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
fi
|
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)ppa([0-9]+)$ ]]; then
|
||||||
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)(-([0-9]+))?$ ]]; then
|
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||||
BASE_VERSION="${BASH_REMATCH[1]}"
|
PPA_NUM="${BASH_REMATCH[2]}"
|
||||||
NEW_VERSION="${BASE_VERSION}ppa1"
|
NEW_PPA_NUM=$((PPA_NUM + 1))
|
||||||
echo " Warning: Native format cannot have Debian revision, converting to PPA format: $CHANGELOG_VERSION -> $NEW_VERSION"
|
NEW_VERSION="${BASE_VERSION}ppa${NEW_PPA_NUM}"
|
||||||
else
|
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
NEW_VERSION="${CHANGELOG_VERSION}ppa1"
|
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)\+git([0-9]+)(\.[a-f0-9]+)?(ppa([0-9]+))?$ ]]; then
|
||||||
echo " Warning: Could not parse version format, appending ppa1: $CHANGELOG_VERSION -> $NEW_VERSION"
|
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||||
fi
|
GIT_NUM="${BASH_REMATCH[2]}"
|
||||||
|
GIT_HASH="${BASH_REMATCH[3]}"
|
||||||
|
PPA_NUM="${BASH_REMATCH[5]}"
|
||||||
|
|
||||||
if [[ -z "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR/debian" ]]; then
|
# Check if old DSC has ppa suffix even if changelog doesn't
|
||||||
echo " Error: Source directory with debian/ not found for version increment"
|
if [[ -z "$PPA_NUM" ]] && [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
|
||||||
exit 1
|
OLD_PPA_NUM="${BASH_REMATCH[1]}"
|
||||||
fi
|
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
|
||||||
SOURCE_CHANGELOG="$SOURCE_DIR/debian/changelog"
|
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
|
||||||
if [[ ! -f "$SOURCE_CHANGELOG" ]]; then
|
elif [[ -n "$PPA_NUM" ]]; then
|
||||||
echo " Error: Changelog not found in source directory: $SOURCE_CHANGELOG"
|
NEW_PPA_NUM=$((PPA_NUM + 1))
|
||||||
exit 1
|
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
|
||||||
fi
|
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
|
else
|
||||||
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa1"
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
{
|
|
||||||
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
|
||||||
echo ""
|
|
||||||
echo " * Rebuild to fix repository metadata issues"
|
|
||||||
echo ""
|
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
|
||||||
echo ""
|
|
||||||
if [[ -f "$REPO_CHANGELOG" ]]; then
|
|
||||||
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
|
|
||||||
if [[ -n "$OLD_ENTRY_START" ]]; then
|
|
||||||
tail -n +$OLD_ENTRY_START "$REPO_CHANGELOG"
|
|
||||||
fi
|
fi
|
||||||
|
elif [[ "$CHANGELOG_VERSION" =~ ^([0-9.]+)(-([0-9]+))?$ ]]; then
|
||||||
|
BASE_VERSION="${BASH_REMATCH[1]}"
|
||||||
|
NEW_VERSION="${BASE_VERSION}ppa1"
|
||||||
|
echo " Warning: Native format cannot have Debian revision, converting to PPA format: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
|
else
|
||||||
|
NEW_VERSION="${CHANGELOG_VERSION}ppa1"
|
||||||
|
echo " Warning: Could not parse version format, appending ppa1: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
fi
|
fi
|
||||||
} > "$TEMP_CHANGELOG"
|
|
||||||
cp "$TEMP_CHANGELOG" "$SOURCE_CHANGELOG"
|
|
||||||
rm -f "$TEMP_CHANGELOG"
|
|
||||||
|
|
||||||
CHANGELOG_VERSION="$NEW_VERSION"
|
if [[ -z "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR/debian" ]]; then
|
||||||
VERSION="$NEW_VERSION"
|
echo " Error: Source directory with debian/ not found for version increment"
|
||||||
COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
|
exit 1
|
||||||
|
|
||||||
for old_tarball in "${PACKAGE}"_*.tar.gz; do
|
|
||||||
if [[ -f "$old_tarball" ]] && [[ "$old_tarball" != "${PACKAGE}_${NEW_VERSION}.tar.gz" ]]; then
|
|
||||||
echo " Removing old tarball from OBS: $old_tarball"
|
|
||||||
osc rm -f "$old_tarball" 2>/dev/null || rm -f "$old_tarball"
|
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$PACKAGE" == "dms" ]] && [[ -f "$WORK_DIR/dms-source.tar.gz" ]]; then
|
SOURCE_CHANGELOG="$SOURCE_DIR/debian/changelog"
|
||||||
echo " Recreating dms-source.tar.gz with new directory name for incremented version"
|
if [[ ! -f "$SOURCE_CHANGELOG" ]]; then
|
||||||
EXPECTED_SOURCE_DIR="DankMaterialShell-${NEW_VERSION}"
|
echo " Error: Changelog not found in source directory: $SOURCE_CHANGELOG"
|
||||||
TEMP_SOURCE_DIR=$(mktemp -d)
|
exit 1
|
||||||
cd "$TEMP_SOURCE_DIR"
|
fi
|
||||||
tar -xzf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xJf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xjf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null
|
|
||||||
EXTRACTED=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
||||||
if [[ -n "$EXTRACTED" ]] && [[ "$EXTRACTED" != "./$EXPECTED_SOURCE_DIR" ]]; then
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
echo " Renaming $EXTRACTED to $EXPECTED_SOURCE_DIR"
|
{
|
||||||
mv "$EXTRACTED" "$EXPECTED_SOURCE_DIR"
|
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
||||||
rm -f "$WORK_DIR/dms-source.tar.gz"
|
echo ""
|
||||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/dms-source.tar.gz" "$EXPECTED_SOURCE_DIR"
|
echo " * Rebuild to fix repository metadata issues"
|
||||||
ROOT_DIR=$(tar -tf "$WORK_DIR/dms-source.tar.gz" | head -1 | cut -d/ -f1)
|
echo ""
|
||||||
if [[ "$ROOT_DIR" != "$EXPECTED_SOURCE_DIR" ]]; then
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
||||||
echo " Error: Recreated tarball has wrong root directory: $ROOT_DIR (expected $EXPECTED_SOURCE_DIR)"
|
echo ""
|
||||||
exit 1
|
if [[ -f "$REPO_CHANGELOG" ]]; then
|
||||||
|
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
|
||||||
|
if [[ -n "$OLD_ENTRY_START" ]]; then
|
||||||
|
tail -n +$OLD_ENTRY_START "$REPO_CHANGELOG"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
} > "$TEMP_CHANGELOG"
|
||||||
cd "$REPO_ROOT"
|
cp "$TEMP_CHANGELOG" "$SOURCE_CHANGELOG"
|
||||||
rm -rf "$TEMP_SOURCE_DIR"
|
rm -f "$TEMP_CHANGELOG"
|
||||||
fi
|
|
||||||
|
|
||||||
echo " Recreating tarball with new version: $COMBINED_TARBALL"
|
CHANGELOG_VERSION="$NEW_VERSION"
|
||||||
if [[ -n "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR/debian" ]]; then
|
VERSION="$NEW_VERSION"
|
||||||
if [[ "$PACKAGE" == "dms" ]]; then
|
COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
|
||||||
cd "$(dirname "$SOURCE_DIR")"
|
|
||||||
CURRENT_DIR=$(basename "$SOURCE_DIR")
|
for old_tarball in "${PACKAGE}"_*.tar.gz; do
|
||||||
EXPECTED_DIR="DankMaterialShell-${NEW_VERSION}"
|
if [[ -f "$old_tarball" ]] && [[ "$old_tarball" != "${PACKAGE}_${NEW_VERSION}.tar.gz" ]]; then
|
||||||
if [[ "$CURRENT_DIR" != "$EXPECTED_DIR" ]]; then
|
echo " Removing old tarball from OBS: $old_tarball"
|
||||||
echo " Renaming directory from $CURRENT_DIR to $EXPECTED_DIR to match debian/rules"
|
osc rm -f "$old_tarball" 2>/dev/null || rm -f "$old_tarball"
|
||||||
if [[ -d "$CURRENT_DIR" ]]; then
|
fi
|
||||||
mv "$CURRENT_DIR" "$EXPECTED_DIR"
|
done
|
||||||
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
|
||||||
else
|
if [[ "$PACKAGE" == "dms" ]] && [[ -f "$WORK_DIR/dms-source.tar.gz" ]]; then
|
||||||
echo " Warning: Source directory $CURRENT_DIR not found, extracting from existing tarball"
|
echo " Recreating dms-source.tar.gz with new directory name for incremented version"
|
||||||
OLD_TARBALL=$(ls "${PACKAGE}"_*.tar.gz 2>/dev/null | head -1)
|
EXPECTED_SOURCE_DIR="DankMaterialShell-${NEW_VERSION}"
|
||||||
if [[ -f "$OLD_TARBALL" ]]; then
|
TEMP_SOURCE_DIR=$(mktemp -d)
|
||||||
EXTRACT_DIR=$(mktemp -d)
|
cd "$TEMP_SOURCE_DIR"
|
||||||
cd "$EXTRACT_DIR"
|
tar -xzf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xJf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null || tar -xjf "$WORK_DIR/dms-source.tar.gz" 2>/dev/null
|
||||||
tar -xzf "$WORK_DIR/$OLD_TARBALL"
|
EXTRACTED=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
||||||
EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
if [[ -n "$EXTRACTED" ]] && [[ "$EXTRACTED" != "./$EXPECTED_SOURCE_DIR" ]]; then
|
||||||
if [[ -n "$EXTRACTED_DIR" ]] && [[ "$EXTRACTED_DIR" != "./$EXPECTED_DIR" ]]; then
|
echo " Renaming $EXTRACTED to $EXPECTED_SOURCE_DIR"
|
||||||
mv "$EXTRACTED_DIR" "$EXPECTED_DIR"
|
mv "$EXTRACTED" "$EXPECTED_SOURCE_DIR"
|
||||||
if [[ -f "$EXPECTED_DIR/debian/changelog" ]]; then
|
rm -f "$WORK_DIR/dms-source.tar.gz"
|
||||||
ACTUAL_VER=$(grep -m1 "^$PACKAGE" "$EXPECTED_DIR/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/')
|
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/dms-source.tar.gz" "$EXPECTED_SOURCE_DIR"
|
||||||
if [[ "$ACTUAL_VER" != "$NEW_VERSION" ]]; then
|
ROOT_DIR=$(tar -tf "$WORK_DIR/dms-source.tar.gz" | head -1 | cut -d/ -f1)
|
||||||
echo " Updating changelog version in extracted directory"
|
if [[ "$ROOT_DIR" != "$EXPECTED_SOURCE_DIR" ]]; then
|
||||||
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
echo " Error: Recreated tarball has wrong root directory: $ROOT_DIR (expected $EXPECTED_SOURCE_DIR)"
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
exit 1
|
||||||
{
|
fi
|
||||||
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
fi
|
||||||
echo ""
|
cd "$REPO_ROOT"
|
||||||
echo " * Rebuild to fix repository metadata issues"
|
rm -rf "$TEMP_SOURCE_DIR"
|
||||||
echo ""
|
fi
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
|
||||||
echo ""
|
echo " Recreating tarball with new version: $COMBINED_TARBALL"
|
||||||
if [[ -f "$REPO_CHANGELOG" ]]; then
|
if [[ -n "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR/debian" ]]; then
|
||||||
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
|
if [[ "$PACKAGE" == "dms" ]]; then
|
||||||
if [[ -n "$OLD_ENTRY_START" ]]; then
|
cd "$(dirname "$SOURCE_DIR")"
|
||||||
tail -n +$OLD_ENTRY_START "$REPO_CHANGELOG"
|
CURRENT_DIR=$(basename "$SOURCE_DIR")
|
||||||
fi
|
EXPECTED_DIR="DankMaterialShell-${NEW_VERSION}"
|
||||||
fi
|
if [[ "$CURRENT_DIR" != "$EXPECTED_DIR" ]]; then
|
||||||
} > "$TEMP_CHANGELOG"
|
echo " Renaming directory from $CURRENT_DIR to $EXPECTED_DIR to match debian/rules"
|
||||||
cp "$TEMP_CHANGELOG" "$EXPECTED_DIR/debian/changelog"
|
if [[ -d "$CURRENT_DIR" ]]; then
|
||||||
rm -f "$TEMP_CHANGELOG"
|
mv "$CURRENT_DIR" "$EXPECTED_DIR"
|
||||||
fi
|
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
||||||
fi
|
|
||||||
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
else
|
|
||||||
echo " Error: Could not extract or find source directory"
|
|
||||||
rm -rf "$EXTRACT_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo " Error: No existing tarball found to extract"
|
echo " Warning: Source directory $CURRENT_DIR not found, extracting from existing tarball"
|
||||||
exit 1
|
OLD_TARBALL=$(ls "${PACKAGE}"_*.tar.gz 2>/dev/null | head -1)
|
||||||
|
if [[ -f "$OLD_TARBALL" ]]; then
|
||||||
|
EXTRACT_DIR=$(mktemp -d)
|
||||||
|
cd "$EXTRACT_DIR"
|
||||||
|
tar -xzf "$WORK_DIR/$OLD_TARBALL"
|
||||||
|
EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
||||||
|
if [[ -n "$EXTRACTED_DIR" ]] && [[ "$EXTRACTED_DIR" != "./$EXPECTED_DIR" ]]; then
|
||||||
|
mv "$EXTRACTED_DIR" "$EXPECTED_DIR"
|
||||||
|
if [[ -f "$EXPECTED_DIR/debian/changelog" ]]; then
|
||||||
|
ACTUAL_VER=$(grep -m1 "^$PACKAGE" "$EXPECTED_DIR/debian/changelog" 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/')
|
||||||
|
if [[ "$ACTUAL_VER" != "$NEW_VERSION" ]]; then
|
||||||
|
echo " Updating changelog version in extracted directory"
|
||||||
|
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
{
|
||||||
|
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
||||||
|
echo ""
|
||||||
|
echo " * Rebuild to fix repository metadata issues"
|
||||||
|
echo ""
|
||||||
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
||||||
|
echo ""
|
||||||
|
if [[ -f "$REPO_CHANGELOG" ]]; then
|
||||||
|
OLD_ENTRY_START=$(grep -n "^$PACKAGE (" "$REPO_CHANGELOG" | sed -n '2p' | cut -d: -f1)
|
||||||
|
if [[ -n "$OLD_ENTRY_START" ]]; then
|
||||||
|
tail -n +$OLD_ENTRY_START "$REPO_CHANGELOG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
} > "$TEMP_CHANGELOG"
|
||||||
|
cp "$TEMP_CHANGELOG" "$EXPECTED_DIR/debian/changelog"
|
||||||
|
rm -f "$TEMP_CHANGELOG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
else
|
||||||
|
echo " Error: Could not extract or find source directory"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
cd "$(dirname "$SOURCE_DIR")"
|
|
||||||
TARBALL_BASE=$(basename "$SOURCE_DIR")
|
|
||||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
|
|
||||||
cd "$WORK_DIR"
|
|
||||||
|
|
||||||
|
rm -f "$WORK_DIR/$COMBINED_TARBALL"
|
||||||
|
|
||||||
|
echo " Creating combined tarball: $COMBINED_TARBALL"
|
||||||
|
cd "$(dirname "$SOURCE_DIR")"
|
||||||
|
TARBALL_BASE=$(basename "$SOURCE_DIR")
|
||||||
|
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "==> Detected same version. Not a manual run, skipping Debian version increment."
|
||||||
|
echo "✅ No changes needed for Debian. Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
|
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
|
||||||
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
|
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
|
||||||
|
|
||||||
@@ -852,10 +884,7 @@ Files:
|
|||||||
$TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL
|
$TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL
|
||||||
EOF
|
EOF
|
||||||
echo " - Updated changelog and recreated tarball with version $NEW_VERSION"
|
echo " - Updated changelog and recreated tarball with version $NEW_VERSION"
|
||||||
else
|
|
||||||
echo " Error: Source directory not found, cannot recreate tarball"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,22 @@ if [ "$CHANGELOG_SERIES" != "$UBUNTU_SERIES" ] && [ "$CHANGELOG_SERIES" != "UNRE
|
|||||||
warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES"
|
warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if this is a manual run or automated
|
||||||
|
IS_MANUAL=false
|
||||||
|
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||||
|
IS_MANUAL=true
|
||||||
|
echo "==> Manual rebuild detected (REBUILD_RELEASE=$REBUILD_RELEASE)"
|
||||||
|
elif [[ -n "${FORCE_REBUILD:-}" ]] && [[ "${FORCE_REBUILD}" == "true" ]]; then
|
||||||
|
IS_MANUAL=true
|
||||||
|
echo "==> Manual workflow trigger detected (FORCE_REBUILD=true)"
|
||||||
|
elif [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" ]]; then
|
||||||
|
IS_MANUAL=true
|
||||||
|
echo "==> Manual workflow trigger detected (workflow_dispatch)"
|
||||||
|
elif [[ -z "${GITHUB_ACTIONS:-}" ]] && [[ -z "${CI:-}" ]]; then
|
||||||
|
IS_MANUAL=true
|
||||||
|
echo "==> Local/manual run detected (not in CI)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Detect package type and update version automatically
|
# Detect package type and update version automatically
|
||||||
cd "$PACKAGE_DIR"
|
cd "$PACKAGE_DIR"
|
||||||
|
|
||||||
@@ -274,7 +290,13 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
|||||||
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/+/\\+/g')
|
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/+/\\+/g')
|
||||||
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
|
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
|
||||||
PPA_NUM=$((BASH_REMATCH[1] + 1))
|
PPA_NUM=$((BASH_REMATCH[1] + 1))
|
||||||
info "Detected rebuild of same commit (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
|
if [[ "$IS_MANUAL" == true ]]; then
|
||||||
|
info "Detected rebuild of same commit (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
|
||||||
|
else
|
||||||
|
info "Detected rebuild of same commit (current: $CURRENT_VERSION). Not a manual run, skipping."
|
||||||
|
success "No changes needed (commit matches)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
info "New commit or first build, using PPA number $PPA_NUM"
|
info "New commit or first build, using PPA number $PPA_NUM"
|
||||||
fi
|
fi
|
||||||
@@ -427,7 +449,13 @@ elif [ -n "$GIT_REPO" ]; then
|
|||||||
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/-/\\-/g')
|
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/-/\\-/g')
|
||||||
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
|
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
|
||||||
PPA_NUM=$((BASH_REMATCH[1] + 1))
|
PPA_NUM=$((BASH_REMATCH[1] + 1))
|
||||||
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
|
if [[ "$IS_MANUAL" == true ]]; then
|
||||||
|
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
|
||||||
|
else
|
||||||
|
info "Detected rebuild of same version (current: $CURRENT_VERSION). Not a manual run, skipping."
|
||||||
|
success "No changes needed (version matches)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
info "New version or first build, using PPA number $PPA_NUM"
|
info "New version or first build, using PPA number $PPA_NUM"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ fi
|
|||||||
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
|
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
|
||||||
|
|
||||||
if [ -z "$CHANGES_FILE" ]; then
|
if [ -z "$CHANGES_FILE" ]; then
|
||||||
error "Changes file not found in $PARENT_DIR"
|
warn "Changes file not found in $PARENT_DIR"
|
||||||
exit 1
|
warn "Assuming build was skipped (no changes needed) and exiting successfully."
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Found changes file: $CHANGES_FILE"
|
info "Found changes file: $CHANGES_FILE"
|
||||||
|
|||||||
36
flake.lock
generated
36
flake.lock
generated
@@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762435535,
|
"lastModified": 1762835999,
|
||||||
"narHash": "sha256-QhzRn7pYN35IFpKjjxJAj3GPJECuC+VLhoGem3ezycc=",
|
"narHash": "sha256-UykYGrGFOFTmDpKTLNxj1wvd1gbDG4TkqLNSbV0TYwk=",
|
||||||
"owner": "AvengeMedia",
|
"owner": "AvengeMedia",
|
||||||
"repo": "dgop",
|
"repo": "dgop",
|
||||||
"rev": "6cf638dde818f9f8a2e26d0243179c43cb3458d7",
|
"rev": "799301991cd5dcea9b64245f9d500dcc76615653",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1762363567,
|
"lastModified": 1764950072,
|
||||||
"narHash": "sha256-YRqMDEtSMbitIMj+JLpheSz0pwEr0Rmy5mC7myl17xs=",
|
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "ae814fd3904b621d8ab97418f1d0f2eb0d3716f4",
|
"rev": "f61125a668a320878494449750330ca58b78c557",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -36,10 +36,32 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"quickshell": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1764663772,
|
||||||
|
"narHash": "sha256-sHqLmm0wAt3PC4vczJeBozI1/f4rv9yp3IjkClHDXDs=",
|
||||||
|
"ref": "refs/heads/master",
|
||||||
|
"rev": "26531fc46ef17e9365b03770edd3fb9206fcb460",
|
||||||
|
"revCount": 713,
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"rev": "26531fc46ef17e9365b03770edd3fb9206fcb460",
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"dgop": "dgop",
|
"dgop": "dgop",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs",
|
||||||
|
"quickshell": "quickshell"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
138
flake.nix
138
flake.nix
@@ -7,12 +7,17 @@
|
|||||||
url = "github:AvengeMedia/dgop";
|
url = "github:AvengeMedia/dgop";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
quickshell = {
|
||||||
|
url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=26531fc46ef17e9365b03770edd3fb9206fcb460";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
dgop,
|
dgop,
|
||||||
|
quickshell,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
forEachSystem = fn:
|
forEachSystem = fn:
|
||||||
@@ -20,8 +25,9 @@
|
|||||||
system: fn system nixpkgs.legacyPackages.${system}
|
system: fn system nixpkgs.legacyPackages.${system}
|
||||||
);
|
);
|
||||||
buildDmsPkgs = pkgs: {
|
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;
|
dgop = dgop.packages.${pkgs.stdenv.hostPlatform.system}.dgop;
|
||||||
|
quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
};
|
};
|
||||||
mkModuleWithDmsPkgs = path: args @ {pkgs, ...}: {
|
mkModuleWithDmsPkgs = path: args @ {pkgs, ...}: {
|
||||||
imports = [
|
imports = [
|
||||||
@@ -46,62 +52,71 @@
|
|||||||
+ "_"
|
+ "_"
|
||||||
+ (self.shortRev or "dirty");
|
+ (self.shortRev or "dirty");
|
||||||
in {
|
in {
|
||||||
dmsCli = pkgs.buildGoModule (finalAttrs: {
|
dms-shell = pkgs.buildGoModule (
|
||||||
inherit version;
|
let
|
||||||
|
rootSrc = ./.;
|
||||||
|
in {
|
||||||
|
inherit version;
|
||||||
|
pname = "dms-shell";
|
||||||
|
src = ./core;
|
||||||
|
vendorHash = "sha256-2PCqiW4frxME8IlmwWH5ktznhd/G1bah5Ae4dp0HPTQ=";
|
||||||
|
|
||||||
pname = "dmsCli";
|
subPackages = ["cmd/dms"];
|
||||||
src = ./core;
|
|
||||||
vendorHash = "sha256-2PCqiW4frxME8IlmwWH5ktznhd/G1bah5Ae4dp0HPTQ=";
|
|
||||||
|
|
||||||
subPackages = ["cmd/dms"];
|
ldflags = [
|
||||||
|
"-s"
|
||||||
|
"-w"
|
||||||
|
"-X main.Version=${version}"
|
||||||
|
];
|
||||||
|
|
||||||
ldflags = [
|
nativeBuildInputs = with pkgs; [
|
||||||
"-s"
|
installShellFiles
|
||||||
"-w"
|
makeWrapper
|
||||||
"-X main.Version=${finalAttrs.version}"
|
];
|
||||||
];
|
|
||||||
|
|
||||||
nativeBuildInputs = [pkgs.installShellFiles];
|
postInstall = ''
|
||||||
|
mkdir -p $out/share/quickshell/dms
|
||||||
|
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||||
|
|
||||||
postInstall = ''
|
chmod u+w $out/share/quickshell/dms/VERSION
|
||||||
installShellCompletion --cmd dms \
|
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||||
--bash <($out/bin/dms completion bash) \
|
|
||||||
--fish <($out/bin/dms completion fish ) \
|
|
||||||
--zsh <($out/bin/dms completion zsh)
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
# Install desktop file and icon
|
||||||
description = "DankMaterialShell Command Line Interface";
|
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||||
homepage = "https://github.com/AvengeMedia/danklinux";
|
$out/share/applications/dms-open.desktop
|
||||||
mainProgram = "dms";
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
license = pkgs.lib.licenses.mit;
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
platforms = pkgs.lib.platforms.unix;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
dankMaterialShell = pkgs.stdenvNoCC.mkDerivation {
|
wrapProgram $out/bin/dms --add-flags "-c $out/share/quickshell/dms"
|
||||||
inherit version;
|
|
||||||
|
|
||||||
pname = "dankMaterialShell";
|
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||||
src = ./quickshell;
|
$out/lib/systemd/user/dms.service
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/etc/xdg/quickshell
|
|
||||||
cp -r ./ $out/etc/xdg/quickshell/dms
|
|
||||||
|
|
||||||
# Create DMS Version file
|
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||||
echo "${version}" > $out/etc/xdg/quickshell/dms/VERSION
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
|
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||||
|
|
||||||
# Install desktop file
|
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||||
mkdir -p $out/share/applications
|
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||||
cp ${./assets/dms-open.desktop} $out/share/applications/dms-open.desktop
|
|
||||||
|
|
||||||
# Install icon
|
installShellCompletion --cmd dms \
|
||||||
mkdir -p $out/share/icons/hicolor/scalable/apps
|
--bash <($out/bin/dms completion bash) \
|
||||||
cp ${./core/assets/danklogo.svg} $out/share/icons/hicolor/scalable/apps/danklogo.svg
|
--fish <($out/bin/dms completion fish) \
|
||||||
'';
|
--zsh <($out/bin/dms completion zsh)
|
||||||
};
|
'';
|
||||||
|
|
||||||
default = self.packages.${system}.dmsCli;
|
meta = {
|
||||||
|
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;
|
||||||
|
mainProgram = "dms";
|
||||||
|
platforms = pkgs.lib.platforms.linux;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
default = self.packages.${system}.dms-shell;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -112,5 +127,38 @@
|
|||||||
nixosModules.dankMaterialShell = mkModuleWithDmsPkgs ./distro/nix/nixos.nix;
|
nixosModules.dankMaterialShell = mkModuleWithDmsPkgs ./distro/nix/nixos.nix;
|
||||||
|
|
||||||
nixosModules.greeter = mkModuleWithDmsPkgs ./distro/nix/greeter.nix;
|
nixosModules.greeter = mkModuleWithDmsPkgs ./distro/nix/greeter.nix;
|
||||||
|
|
||||||
|
devShells = forEachSystem (
|
||||||
|
system: pkgs: let
|
||||||
|
qmlPkgs =
|
||||||
|
[
|
||||||
|
quickshell.packages.${system}.default
|
||||||
|
]
|
||||||
|
++ (with pkgs.kdePackages; [
|
||||||
|
qtdeclarative
|
||||||
|
kirigami.unwrapped
|
||||||
|
sonnet
|
||||||
|
qtmultimedia
|
||||||
|
]);
|
||||||
|
in {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs;
|
||||||
|
[
|
||||||
|
go_1_24
|
||||||
|
gopls
|
||||||
|
delve
|
||||||
|
go-tools
|
||||||
|
gnumake
|
||||||
|
]
|
||||||
|
++ qmlPkgs;
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
touch quickshell/.qmlls.ini 2>/dev/null
|
||||||
|
'';
|
||||||
|
|
||||||
|
QML2_IMPORT_PATH = pkgs.lib.concatStringsSep ":" (map (o: "${o}/lib/qt-6/qml") qmlPkgs);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const KEY_MAP = {
|
|||||||
96: "grave",
|
96: "grave",
|
||||||
32: "space",
|
32: "space",
|
||||||
16777225: "Print",
|
16777225: "Print",
|
||||||
|
16777226: "Print",
|
||||||
16777220: "Return",
|
16777220: "Return",
|
||||||
16777221: "Return",
|
16777221: "Return",
|
||||||
16777217: "Tab",
|
16777217: "Tab",
|
||||||
@@ -93,20 +94,20 @@ function xkbKeyFromQtKey(qk) {
|
|||||||
|
|
||||||
function modsFromEvent(mods) {
|
function modsFromEvent(mods) {
|
||||||
var result = [];
|
var result = [];
|
||||||
if (mods & 0x04000000)
|
|
||||||
result.push("Ctrl");
|
|
||||||
if (mods & 0x02000000)
|
|
||||||
result.push("Shift");
|
|
||||||
var hasAlt = mods & 0x08000000;
|
var hasAlt = mods & 0x08000000;
|
||||||
var hasSuper = mods & 0x10000000;
|
var hasSuper = mods & 0x10000000;
|
||||||
if (hasAlt && hasSuper) {
|
if (hasAlt && hasSuper) {
|
||||||
result.push("Mod");
|
result.push("Mod");
|
||||||
} else {
|
} else {
|
||||||
if (hasAlt)
|
|
||||||
result.push("Alt");
|
|
||||||
if (hasSuper)
|
if (hasSuper)
|
||||||
result.push("Super");
|
result.push("Super");
|
||||||
|
if (hasAlt)
|
||||||
|
result.push("Alt");
|
||||||
}
|
}
|
||||||
|
if (mods & 0x04000000)
|
||||||
|
result.push("Ctrl");
|
||||||
|
if (mods & 0x02000000)
|
||||||
|
result.push("Shift");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const DMS_ACTIONS = [
|
|||||||
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
|
{ 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 open", label: "Notepad: Open" },
|
||||||
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
|
{ 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 toggle \"\"", label: "Dashboard: Toggle" },
|
||||||
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
|
{ 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 media", label: "Dashboard: Media" },
|
||||||
{ id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" },
|
{ id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" },
|
||||||
@@ -59,6 +59,7 @@ const DMS_ACTIONS = [
|
|||||||
{ id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" },
|
{ 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 mute", label: "Volume Mute Toggle" },
|
||||||
{ id: "spawn dms ipc call audio micmute", label: "Microphone 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", label: "Brightness Up" },
|
||||||
{ id: "spawn dms ipc call brightness increment 1", label: "Brightness Up (1%)" },
|
{ 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 5", label: "Brightness Up (5%)" },
|
||||||
@@ -108,9 +109,15 @@ const COMPOSITOR_ACTIONS = {
|
|||||||
{ id: "fullscreen-window", label: "Fullscreen" },
|
{ id: "fullscreen-window", label: "Fullscreen" },
|
||||||
{ id: "maximize-column", label: "Maximize Column" },
|
{ id: "maximize-column", label: "Maximize Column" },
|
||||||
{ id: "center-column", label: "Center Column" },
|
{ id: "center-column", label: "Center Column" },
|
||||||
|
{ id: "center-visible-columns", label: "Center Visible Columns" },
|
||||||
{ id: "toggle-window-floating", label: "Toggle Floating" },
|
{ 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-column-width", label: "Cycle Column Width" },
|
||||||
{ id: "switch-preset-window-height", label: "Cycle Window Height" },
|
{ 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-left", label: "Consume/Expel Left" },
|
||||||
{ id: "consume-or-expel-window-right", label: "Consume/Expel Right" },
|
{ id: "consume-or-expel-window-right", label: "Consume/Expel Right" },
|
||||||
{ id: "toggle-column-tabbed-display", label: "Toggle Tabbed" }
|
{ id: "toggle-column-tabbed-display", label: "Toggle Tabbed" }
|
||||||
@@ -135,8 +142,10 @@ const COMPOSITOR_ACTIONS = {
|
|||||||
{ id: "focus-workspace-down", label: "Focus Workspace Down" },
|
{ id: "focus-workspace-down", label: "Focus Workspace Down" },
|
||||||
{ id: "focus-workspace-up", label: "Focus Workspace Up" },
|
{ id: "focus-workspace-up", label: "Focus Workspace Up" },
|
||||||
{ id: "focus-workspace-previous", label: "Focus Previous Workspace" },
|
{ 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-down", label: "Move to Workspace Down" },
|
||||||
{ id: "move-column-to-workspace-up", label: "Move to Workspace Up" },
|
{ 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-down", label: "Move Workspace Down" },
|
||||||
{ id: "move-workspace-up", label: "Move Workspace Up" }
|
{ id: "move-workspace-up", label: "Move Workspace Up" }
|
||||||
],
|
],
|
||||||
@@ -172,6 +181,64 @@ const COMPOSITOR_ACTIONS = {
|
|||||||
|
|
||||||
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"];
|
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 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"move-column-to-workspace-down": {
|
||||||
|
args: [{ name: "focus", type: "bool", label: "Follow focus", default: false }]
|
||||||
|
},
|
||||||
|
"move-column-to-workspace-up": {
|
||||||
|
args: [{ name: "focus", type: "bool", label: "Follow focus", default: false }]
|
||||||
|
},
|
||||||
|
"screenshot": {
|
||||||
|
args: [{ name: "show-pointer", type: "bool", label: "Show pointer" }]
|
||||||
|
},
|
||||||
|
"screenshot-screen": {
|
||||||
|
args: [
|
||||||
|
{ name: "show-pointer", type: "bool", label: "Show pointer" },
|
||||||
|
{ name: "write-to-disk", type: "bool", label: "Save to disk" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"screenshot-window": {
|
||||||
|
args: [
|
||||||
|
{ name: "show-pointer", type: "bool", label: "Show pointer" },
|
||||||
|
{ name: "write-to-disk", type: "bool", label: "Save to disk" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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() {
|
function getActionTypes() {
|
||||||
return ACTION_TYPES;
|
return ACTION_TYPES;
|
||||||
}
|
}
|
||||||
@@ -233,11 +300,12 @@ function getActionLabel(action) {
|
|||||||
if (!action)
|
if (!action)
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
const dmsAct = findDmsAction(action);
|
var dmsAct = findDmsAction(action);
|
||||||
if (dmsAct)
|
if (dmsAct)
|
||||||
return dmsAct.label;
|
return dmsAct.label;
|
||||||
|
|
||||||
const compAct = findCompositorAction(action);
|
var base = action.split(" ")[0];
|
||||||
|
var compAct = findCompositorAction(base);
|
||||||
if (compAct)
|
if (compAct)
|
||||||
return compAct.label;
|
return compAct.label;
|
||||||
|
|
||||||
@@ -282,7 +350,8 @@ function isValidAction(action) {
|
|||||||
function isKnownCompositorAction(action) {
|
function isKnownCompositorAction(action) {
|
||||||
if (!action)
|
if (!action)
|
||||||
return false;
|
return false;
|
||||||
return findCompositorAction(action) !== null;
|
var base = action.split(" ")[0];
|
||||||
|
return findCompositorAction(base) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSpawnAction(command, args) {
|
function buildSpawnAction(command, args) {
|
||||||
@@ -321,3 +390,135 @@ function parseShellCommand(action) {
|
|||||||
content = content.slice(1, -1);
|
content = content.slice(1, -1);
|
||||||
return content.replace(/\\"/g, "\"");
|
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 argParts = parts.slice(1);
|
||||||
|
|
||||||
|
switch (base) {
|
||||||
|
case "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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "move-column-to-workspace-down":
|
||||||
|
case "move-column-to-workspace-up":
|
||||||
|
for (var k = 0; k < argParts.length; k++) {
|
||||||
|
if (argParts[k] === "focus=true" || argParts[k] === "focus=false")
|
||||||
|
args.focus = argParts[k] === "focus=true";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (base.startsWith("screenshot")) {
|
||||||
|
for (var j = 0; j < argParts.length; j++) {
|
||||||
|
var kv = argParts[j].split("=");
|
||||||
|
if (kv.length === 2)
|
||||||
|
args[kv[0]] = kv[1] === "true";
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
switch (base) {
|
||||||
|
case "move-column-to-workspace":
|
||||||
|
if (args.index)
|
||||||
|
parts.push(args.index);
|
||||||
|
if (args.focus === false)
|
||||||
|
parts.push("focus=false");
|
||||||
|
break;
|
||||||
|
case "move-column-to-workspace-down":
|
||||||
|
case "move-column-to-workspace-up":
|
||||||
|
if (args.focus === false)
|
||||||
|
parts.push("focus=false");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (base.startsWith("screenshot")) {
|
||||||
|
if (args["show-pointer"] === true)
|
||||||
|
parts.push("show-pointer=true");
|
||||||
|
if (args["write-to-disk"] === true)
|
||||||
|
parts.push("write-to-disk=true");
|
||||||
|
} 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];
|
const currentOSD = currentOSDsByScreen[screenName];
|
||||||
|
|
||||||
if (currentOSD && currentOSD !== osd) {
|
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;
|
currentOSDsByScreen[screenName] = osd;
|
||||||
|
|||||||
@@ -3,122 +3,139 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property int noTimeout: -1
|
||||||
property int defaultDebounceMs: 50
|
property int defaultDebounceMs: 50
|
||||||
property int defaultTimeoutMs: 10000
|
property int defaultTimeoutMs: 10000
|
||||||
property var _procDebouncers: ({})
|
property var _procDebouncers: ({})
|
||||||
|
|
||||||
function runCommand(id, command, callback, debounceMs, timeoutMs) {
|
function runCommand(id, command, callback, debounceMs, timeoutMs) {
|
||||||
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs
|
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs;
|
||||||
const timeout = (typeof timeoutMs === "number" && timeoutMs > 0) ? timeoutMs : defaultTimeoutMs
|
const timeout = (typeof timeoutMs === "number") ? timeoutMs : defaultTimeoutMs;
|
||||||
let procId = id ? id : Math.random()
|
let procId = id ? id : Math.random();
|
||||||
const isRandomId = !id
|
const isRandomId = !id;
|
||||||
|
|
||||||
if (!_procDebouncers[procId]) {
|
if (!_procDebouncers[procId]) {
|
||||||
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
|
const t = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root);
|
||||||
t.triggered.connect(function() { _launchProc(procId, isRandomId) })
|
t.triggered.connect(function () {
|
||||||
_procDebouncers[procId] = { timer: t, command: command, callback: callback, waitMs: wait, timeoutMs: timeout, isRandomId: isRandomId }
|
_launchProc(procId, isRandomId);
|
||||||
|
});
|
||||||
|
_procDebouncers[procId] = {
|
||||||
|
timer: t,
|
||||||
|
command: command,
|
||||||
|
callback: callback,
|
||||||
|
waitMs: wait,
|
||||||
|
timeoutMs: timeout,
|
||||||
|
isRandomId: isRandomId
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
_procDebouncers[procId].command = command
|
_procDebouncers[procId].command = command;
|
||||||
_procDebouncers[procId].callback = callback
|
_procDebouncers[procId].callback = callback;
|
||||||
_procDebouncers[procId].waitMs = wait
|
_procDebouncers[procId].waitMs = wait;
|
||||||
_procDebouncers[procId].timeoutMs = timeout
|
_procDebouncers[procId].timeoutMs = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = _procDebouncers[procId]
|
const entry = _procDebouncers[procId];
|
||||||
entry.timer.interval = entry.waitMs
|
entry.timer.interval = entry.waitMs;
|
||||||
entry.timer.restart()
|
entry.timer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _launchProc(id, isRandomId) {
|
function _launchProc(id, isRandomId) {
|
||||||
const entry = _procDebouncers[id]
|
const entry = _procDebouncers[id];
|
||||||
if (!entry) return
|
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)
|
proc.stdout = out;
|
||||||
const out = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
|
proc.stderr = err;
|
||||||
const err = Qt.createQmlObject('import Quickshell.Io; StdioCollector {}', proc)
|
proc.command = entry.command;
|
||||||
const timeoutTimer = Qt.createQmlObject('import QtQuick; Timer { repeat: false }', root)
|
|
||||||
|
|
||||||
proc.stdout = out
|
let capturedOut = "";
|
||||||
proc.stderr = err
|
let capturedErr = "";
|
||||||
proc.command = entry.command
|
let exitSeen = false;
|
||||||
|
let exitCodeValue = -1;
|
||||||
|
let outSeen = false;
|
||||||
|
let errSeen = false;
|
||||||
|
let timedOut = false;
|
||||||
|
|
||||||
let capturedOut = ""
|
timeoutTimer.interval = entry.timeoutMs;
|
||||||
let capturedErr = ""
|
timeoutTimer.triggered.connect(function () {
|
||||||
let exitSeen = false
|
|
||||||
let exitCodeValue = -1
|
|
||||||
let outSeen = false
|
|
||||||
let errSeen = false
|
|
||||||
let timedOut = false
|
|
||||||
|
|
||||||
timeoutTimer.interval = entry.timeoutMs
|
|
||||||
timeoutTimer.triggered.connect(function() {
|
|
||||||
if (!exitSeen) {
|
if (!exitSeen) {
|
||||||
timedOut = true
|
timedOut = true;
|
||||||
proc.running = false
|
proc.running = false;
|
||||||
exitSeen = true
|
exitSeen = true;
|
||||||
exitCodeValue = 124
|
exitCodeValue = 124;
|
||||||
maybeComplete()
|
maybeComplete();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
out.streamFinished.connect(function() {
|
out.streamFinished.connect(function () {
|
||||||
try {
|
try {
|
||||||
capturedOut = out.text || ""
|
capturedOut = out.text || "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
capturedOut = ""
|
capturedOut = "";
|
||||||
}
|
}
|
||||||
outSeen = true
|
outSeen = true;
|
||||||
maybeComplete()
|
maybeComplete();
|
||||||
})
|
});
|
||||||
|
|
||||||
err.streamFinished.connect(function() {
|
err.streamFinished.connect(function () {
|
||||||
try {
|
try {
|
||||||
capturedErr = err.text || ""
|
capturedErr = err.text || "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
capturedErr = ""
|
capturedErr = "";
|
||||||
}
|
}
|
||||||
errSeen = true
|
errSeen = true;
|
||||||
maybeComplete()
|
maybeComplete();
|
||||||
})
|
});
|
||||||
|
|
||||||
proc.exited.connect(function(code) {
|
proc.exited.connect(function (code) {
|
||||||
timeoutTimer.stop()
|
timeoutTimer.stop();
|
||||||
exitSeen = true
|
exitSeen = true;
|
||||||
exitCodeValue = code
|
exitCodeValue = code;
|
||||||
maybeComplete()
|
maybeComplete();
|
||||||
})
|
});
|
||||||
|
|
||||||
function maybeComplete() {
|
function maybeComplete() {
|
||||||
if (!exitSeen || !outSeen || !errSeen) return
|
if (!exitSeen || !outSeen || !errSeen)
|
||||||
timeoutTimer.stop()
|
return;
|
||||||
|
timeoutTimer.stop();
|
||||||
if (entry && entry.callback && typeof entry.callback === "function") {
|
if (entry && entry.callback && typeof entry.callback === "function") {
|
||||||
try {
|
try {
|
||||||
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : ""
|
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : "";
|
||||||
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1
|
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1;
|
||||||
entry.callback(safeOutput, safeExitCode)
|
entry.callback(safeOutput, safeExitCode);
|
||||||
} catch (e) {
|
} 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 {
|
||||||
try { timeoutTimer.destroy() } catch (_) {}
|
proc.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
timeoutTimer.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
if (isRandomId || entry.isRandomId) {
|
if (isRandomId || entry.isRandomId) {
|
||||||
Qt.callLater(function() {
|
Qt.callLater(function () {
|
||||||
if (_procDebouncers[id]) {
|
if (_procDebouncers[id]) {
|
||||||
try { _procDebouncers[id].timer.destroy() } catch (_) {}
|
try {
|
||||||
delete _procDebouncers[id]
|
_procDebouncers[id].timer.destroy();
|
||||||
|
} catch (_) {}
|
||||||
|
delete _procDebouncers[id];
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proc.running = true
|
proc.running = true;
|
||||||
timeoutTimer.start()
|
if (entry.timeoutMs !== noTimeout)
|
||||||
|
timeoutTimer.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ Singleton {
|
|||||||
property bool controlCenterShowNetworkIcon: true
|
property bool controlCenterShowNetworkIcon: true
|
||||||
property bool controlCenterShowBluetoothIcon: true
|
property bool controlCenterShowBluetoothIcon: true
|
||||||
property bool controlCenterShowAudioIcon: true
|
property bool controlCenterShowAudioIcon: true
|
||||||
property bool controlCenterShowVpnIcon: false
|
property bool controlCenterShowVpnIcon: true
|
||||||
property bool controlCenterShowBrightnessIcon: false
|
property bool controlCenterShowBrightnessIcon: false
|
||||||
property bool controlCenterShowMicIcon: false
|
property bool controlCenterShowMicIcon: false
|
||||||
property bool controlCenterShowBatteryIcon: false
|
property bool controlCenterShowBatteryIcon: false
|
||||||
@@ -248,10 +248,12 @@ Singleton {
|
|||||||
property int acLockTimeout: 0
|
property int acLockTimeout: 0
|
||||||
property int acSuspendTimeout: 0
|
property int acSuspendTimeout: 0
|
||||||
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||||
|
property string acProfileName: ""
|
||||||
property int batteryMonitorTimeout: 0
|
property int batteryMonitorTimeout: 0
|
||||||
property int batteryLockTimeout: 0
|
property int batteryLockTimeout: 0
|
||||||
property int batterySuspendTimeout: 0
|
property int batterySuspendTimeout: 0
|
||||||
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||||
|
property string batteryProfileName: ""
|
||||||
property bool lockBeforeSuspend: false
|
property bool lockBeforeSuspend: false
|
||||||
property bool preventIdleForMedia: false
|
property bool preventIdleForMedia: false
|
||||||
property bool loginctlLockIntegration: true
|
property bool loginctlLockIntegration: true
|
||||||
@@ -293,8 +295,10 @@ Singleton {
|
|||||||
|
|
||||||
property bool lockScreenShowPowerActions: true
|
property bool lockScreenShowPowerActions: true
|
||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 3
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
|
property string lockScreenActiveMonitor: "all"
|
||||||
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property bool hideBrightnessSlider: false
|
property bool hideBrightnessSlider: false
|
||||||
|
|
||||||
property int notificationTimeoutLow: 5000
|
property int notificationTimeoutLow: 5000
|
||||||
@@ -311,9 +315,10 @@ Singleton {
|
|||||||
property bool osdMicMuteEnabled: true
|
property bool osdMicMuteEnabled: true
|
||||||
property bool osdCapsLockEnabled: true
|
property bool osdCapsLockEnabled: true
|
||||||
property bool osdPowerProfileEnabled: true
|
property bool osdPowerProfileEnabled: true
|
||||||
|
property bool osdAudioOutputEnabled: true
|
||||||
|
|
||||||
property bool powerActionConfirm: true
|
property bool powerActionConfirm: true
|
||||||
property int powerActionHoldDuration: 1
|
property real powerActionHoldDuration: 0.5
|
||||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||||
property string powerMenuDefaultAction: "logout"
|
property string powerMenuDefaultAction: "logout"
|
||||||
property bool powerMenuGridLayout: false
|
property bool powerMenuGridLayout: false
|
||||||
@@ -367,7 +372,8 @@ Singleton {
|
|||||||
openOnOverview: false,
|
openOnOverview: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
popupGapsAuto: true,
|
popupGapsAuto: true,
|
||||||
popupGapsManual: 4
|
popupGapsManual: 4,
|
||||||
|
maximizeDetection: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -820,18 +820,35 @@ Singleton {
|
|||||||
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
|
"runUserTemplates": (typeof SettingsData !== "undefined") ? SettingsData.runUserMatugenTemplates : true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stockColors) {
|
|
||||||
desired.stockColors = JSON.stringify(stockColors);
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = JSON.stringify(desired);
|
|
||||||
const desiredPath = stateDir + "/matugen.desired.json";
|
|
||||||
const syncModeWithPortal = (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal) ? "true" : "false";
|
|
||||||
const terminalsAlwaysDark = (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) ? "true" : "false";
|
|
||||||
|
|
||||||
console.log("Theme: Starting matugen worker");
|
console.log("Theme: Starting matugen worker");
|
||||||
workerRunning = true;
|
workerRunning = true;
|
||||||
systemThemeGenerator.command = ["sh", "-c", `mkdir -p '${stateDir}' && cat > '${desiredPath}' << 'EOF'\n${json}\nEOF\nexec '${shellDir}/scripts/matugen-worker.sh' '${stateDir}' '${shellDir}' '${configDir}' '${syncModeWithPortal}' '${terminalsAlwaysDark}' --run`];
|
|
||||||
|
const args = [
|
||||||
|
"dms", "matugen", "queue",
|
||||||
|
"--state-dir", stateDir,
|
||||||
|
"--shell-dir", shellDir,
|
||||||
|
"--config-dir", configDir,
|
||||||
|
"--kind", desired.kind,
|
||||||
|
"--value", desired.value,
|
||||||
|
"--mode", desired.mode,
|
||||||
|
"--icon-theme", desired.iconTheme,
|
||||||
|
"--matugen-type", desired.matugenType,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!desired.runUserTemplates) {
|
||||||
|
args.push("--run-user-templates=false");
|
||||||
|
}
|
||||||
|
if (stockColors) {
|
||||||
|
args.push("--stock-colors", JSON.stringify(stockColors));
|
||||||
|
}
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.syncModeWithPortal) {
|
||||||
|
args.push("--sync-mode-with-portal");
|
||||||
|
}
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
||||||
|
args.push("--terminals-always-dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
systemThemeGenerator.command = args;
|
||||||
systemThemeGenerator.running = true;
|
systemThemeGenerator.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ var SPEC = {
|
|||||||
controlCenterShowNetworkIcon: { def: true },
|
controlCenterShowNetworkIcon: { def: true },
|
||||||
controlCenterShowBluetoothIcon: { def: true },
|
controlCenterShowBluetoothIcon: { def: true },
|
||||||
controlCenterShowAudioIcon: { def: true },
|
controlCenterShowAudioIcon: { def: true },
|
||||||
controlCenterShowVpnIcon: { def: false },
|
controlCenterShowVpnIcon: { def: true },
|
||||||
controlCenterShowBrightnessIcon: { def: false },
|
controlCenterShowBrightnessIcon: { def: false },
|
||||||
controlCenterShowMicIcon: { def: false },
|
controlCenterShowMicIcon: { def: false },
|
||||||
controlCenterShowBatteryIcon: { def: false },
|
controlCenterShowBatteryIcon: { def: false },
|
||||||
@@ -147,10 +147,12 @@ var SPEC = {
|
|||||||
acLockTimeout: { def: 0 },
|
acLockTimeout: { def: 0 },
|
||||||
acSuspendTimeout: { def: 0 },
|
acSuspendTimeout: { def: 0 },
|
||||||
acSuspendBehavior: { def: 0 },
|
acSuspendBehavior: { def: 0 },
|
||||||
|
acProfileName: { def: "" },
|
||||||
batteryMonitorTimeout: { def: 0 },
|
batteryMonitorTimeout: { def: 0 },
|
||||||
batteryLockTimeout: { def: 0 },
|
batteryLockTimeout: { def: 0 },
|
||||||
batterySuspendTimeout: { def: 0 },
|
batterySuspendTimeout: { def: 0 },
|
||||||
batterySuspendBehavior: { def: 0 },
|
batterySuspendBehavior: { def: 0 },
|
||||||
|
batteryProfileName: { def: "" },
|
||||||
lockBeforeSuspend: { def: false },
|
lockBeforeSuspend: { def: false },
|
||||||
preventIdleForMedia: { def: false },
|
preventIdleForMedia: { def: false },
|
||||||
loginctlLockIntegration: { def: true },
|
loginctlLockIntegration: { def: true },
|
||||||
@@ -192,8 +194,10 @@ var SPEC = {
|
|||||||
|
|
||||||
lockScreenShowPowerActions: { def: true },
|
lockScreenShowPowerActions: { def: true },
|
||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 3 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
hideBrightnessSlider: { def: false },
|
hideBrightnessSlider: { def: false },
|
||||||
|
|
||||||
notificationTimeoutLow: { def: 5000 },
|
notificationTimeoutLow: { def: 5000 },
|
||||||
@@ -210,9 +214,10 @@ var SPEC = {
|
|||||||
osdMicMuteEnabled: { def: true },
|
osdMicMuteEnabled: { def: true },
|
||||||
osdCapsLockEnabled: { def: true },
|
osdCapsLockEnabled: { def: true },
|
||||||
osdPowerProfileEnabled: { def: false },
|
osdPowerProfileEnabled: { def: false },
|
||||||
|
osdAudioOutputEnabled: { def: true },
|
||||||
|
|
||||||
powerActionConfirm: { def: true },
|
powerActionConfirm: { def: true },
|
||||||
powerActionHoldDuration: { def: 1 },
|
powerActionHoldDuration: { def: 0.5 },
|
||||||
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
|
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
|
||||||
powerMenuDefaultAction: { def: "logout" },
|
powerMenuDefaultAction: { def: "logout" },
|
||||||
powerMenuGridLayout: { def: false },
|
powerMenuGridLayout: { def: false },
|
||||||
@@ -265,7 +270,8 @@ var SPEC = {
|
|||||||
openOnOverview: false,
|
openOnOverview: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
popupGapsAuto: true,
|
popupGapsAuto: true,
|
||||||
popupGapsManual: 4
|
popupGapsManual: 4,
|
||||||
|
maximizeDetection: true
|
||||||
}], onChange: "updateBarConfigs" }
|
}], onChange: "updateBarConfigs" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -700,6 +700,14 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: AudioOutputOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: hyprlandOverviewLoader
|
id: hyprlandOverviewLoader
|
||||||
active: CompositorService.isHyprland
|
active: CompositorService.isHyprland
|
||||||
|
|||||||
@@ -648,6 +648,13 @@ Item {
|
|||||||
return "SETTINGS_OPEN_SUCCESS";
|
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 {
|
function close(): string {
|
||||||
PopoutService.closeSettings();
|
PopoutService.closeSettings();
|
||||||
return "SETTINGS_CLOSE_SUCCESS";
|
return "SETTINGS_CLOSE_SUCCESS";
|
||||||
@@ -658,11 +665,47 @@ Item {
|
|||||||
return "SETTINGS_TOGGLE_SUCCESS";
|
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 {
|
function focusOrToggle(): string {
|
||||||
PopoutService.focusOrToggleSettings();
|
PopoutService.focusOrToggleSettings();
|
||||||
return "SETTINGS_FOCUS_OR_TOGGLE_SUCCESS";
|
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 {
|
function get(key: string): string {
|
||||||
return JSON.stringify(SettingsData?.[key]);
|
return JSON.stringify(SettingsData?.[key]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ DankModal {
|
|||||||
modalWidth: 520
|
modalWidth: 520
|
||||||
modalHeight: 500
|
modalHeight: 500
|
||||||
|
|
||||||
|
onBackgroundClicked: close()
|
||||||
|
|
||||||
onDialogClosed: {
|
onDialogClosed: {
|
||||||
searchQuery = ""
|
searchQuery = ""
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
keyboardNavigationActive: false
|
keyboardNavigationActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user