mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-14 08:12:46 -04:00
Compare commits
46 Commits
| 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 |
@@ -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
|
||||||
+4
-33
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -472,5 +471,8 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
colorCmd,
|
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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-7
@@ -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) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package colorpicker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -10,6 +9,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
"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/wlr_screencopy"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
"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"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,15 +33,19 @@ type Output struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LayerSurface struct {
|
type LayerSurface struct {
|
||||||
output *Output
|
output *Output
|
||||||
state *SurfaceState
|
state *SurfaceState
|
||||||
wlSurface *client.Surface
|
wlSurface *client.Surface
|
||||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
viewport *wp_viewporter.WpViewport
|
viewport *wp_viewporter.WpViewport
|
||||||
wlPool *client.ShmPool
|
wlPool *client.ShmPool
|
||||||
wlBuffer *client.Buffer
|
wlBuffer *client.Buffer
|
||||||
configured bool
|
bufferBusy bool
|
||||||
hidden bool
|
oldPool *client.ShmPool
|
||||||
|
oldBuffer *client.Buffer
|
||||||
|
scopyBuffer *client.Buffer
|
||||||
|
configured bool
|
||||||
|
hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Picker struct {
|
type Picker struct {
|
||||||
@@ -111,6 +115,11 @@ func (p *Picker) Run() (*Color, error) {
|
|||||||
return nil, fmt.Errorf("roundtrip: %w", err)
|
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 {
|
if err := p.createSurfaces(); err != nil {
|
||||||
return nil, fmt.Errorf("create surfaces: %w", err)
|
return nil, fmt.Errorf("create surfaces: %w", err)
|
||||||
}
|
}
|
||||||
@@ -165,26 +174,7 @@ func (p *Picker) connect() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) roundtrip() error {
|
func (p *Picker) roundtrip() error {
|
||||||
callback, err := p.display.Sync()
|
return wlhelpers.Roundtrip(p.display, p.ctx)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
callback.SetDoneHandler(func(e client.CallbackDoneEvent) {
|
|
||||||
close(done)
|
|
||||||
})
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
if err := p.ctx.Dispatch(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) setupRegistry() error {
|
func (p *Picker) setupRegistry() error {
|
||||||
@@ -419,15 +409,10 @@ func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
|
|||||||
|
|
||||||
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
|
func (p *Picker) computeSurfaceScale(ls *LayerSurface) int32 {
|
||||||
out := ls.output
|
out := ls.output
|
||||||
if out == nil || out.fractionalScale <= 0 {
|
if out == nil || out.scale <= 0 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
return out.scale
|
||||||
scale := int32(math.Ceil(out.fractionalScale))
|
|
||||||
if scale <= 0 {
|
|
||||||
scale = 1
|
|
||||||
}
|
|
||||||
return scale
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
|
func (p *Picker) ensureShortcutsInhibitor(ls *LayerSurface) {
|
||||||
@@ -481,6 +466,12 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ls.scopyBuffer != nil {
|
||||||
|
ls.scopyBuffer.Destroy()
|
||||||
|
}
|
||||||
|
ls.scopyBuffer = wlBuffer
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {})
|
||||||
|
|
||||||
if err := frame.Copy(wlBuffer); err != nil {
|
if err := frame.Copy(wlBuffer); err != nil {
|
||||||
log.Error("failed to copy frame", "err", err)
|
log.Error("failed to copy frame", "err", err)
|
||||||
}
|
}
|
||||||
@@ -493,6 +484,13 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
|
|
||||||
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||||
ls.state.OnScreencopyReady()
|
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)
|
scale := p.computeSurfaceScale(ls)
|
||||||
ls.state.SetScale(scale)
|
ls.state.SetScale(scale)
|
||||||
frame.Destroy()
|
frame.Destroy()
|
||||||
@@ -507,7 +505,6 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||||
var renderBuf *ShmBuffer
|
var renderBuf *ShmBuffer
|
||||||
if ls.hidden {
|
if ls.hidden {
|
||||||
// When hidden, just show the screenshot without overlay
|
|
||||||
renderBuf = ls.state.RedrawScreenOnly()
|
renderBuf = ls.state.RedrawScreenOnly()
|
||||||
} else {
|
} else {
|
||||||
renderBuf = ls.state.Redraw()
|
renderBuf = ls.state.Redraw()
|
||||||
@@ -516,65 +513,58 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ls.wlPool != nil {
|
if ls.oldBuffer != nil {
|
||||||
ls.wlPool.Destroy()
|
ls.oldBuffer.Destroy()
|
||||||
ls.wlPool = nil
|
ls.oldBuffer = nil
|
||||||
}
|
}
|
||||||
if ls.wlBuffer != nil {
|
if ls.oldPool != nil {
|
||||||
ls.wlBuffer.Destroy()
|
ls.oldPool.Destroy()
|
||||||
ls.wlBuffer = nil
|
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()))
|
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ls.wlPool = pool
|
ls.wlPool = pool
|
||||||
|
|
||||||
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(FormatARGB8888))
|
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ls.wlBuffer = wlBuffer
|
ls.wlBuffer = wlBuffer
|
||||||
|
|
||||||
|
lsRef := ls
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
lsRef.bufferBusy = false
|
||||||
|
})
|
||||||
|
ls.bufferBusy = true
|
||||||
|
|
||||||
logicalW, logicalH := ls.state.LogicalSize()
|
logicalW, logicalH := ls.state.LogicalSize()
|
||||||
if logicalW == 0 || logicalH == 0 {
|
if logicalW == 0 || logicalH == 0 {
|
||||||
logicalW = int(ls.output.width)
|
logicalW = int(ls.output.width)
|
||||||
logicalH = int(ls.output.height)
|
logicalH = int(ls.output.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
scale := ls.state.Scale()
|
|
||||||
if scale <= 0 {
|
|
||||||
scale = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ls.viewport != nil {
|
if ls.viewport != nil {
|
||||||
srcW := float64(renderBuf.Width) / float64(scale)
|
_ = ls.wlSurface.SetBufferScale(1)
|
||||||
srcH := float64(renderBuf.Height) / float64(scale)
|
_ = ls.viewport.SetSource(0, 0, float64(renderBuf.Width), float64(renderBuf.Height))
|
||||||
if err := ls.viewport.SetSource(0, 0, srcW, srcH); err != nil {
|
_ = ls.viewport.SetDestination(int32(logicalW), int32(logicalH))
|
||||||
log.Warn("failed to set viewport source", "err", err)
|
|
||||||
}
|
|
||||||
if err := ls.viewport.SetDestination(int32(logicalW), int32(logicalH)); err != nil {
|
|
||||||
log.Warn("failed to set viewport destination", "err", err)
|
|
||||||
}
|
|
||||||
if err := ls.wlSurface.SetBufferScale(scale); err != nil {
|
|
||||||
log.Warn("failed to set buffer scale", "err", err)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if err := ls.wlSurface.SetBufferScale(scale); err != nil {
|
bufferScale := ls.output.scale
|
||||||
log.Warn("failed to set buffer scale", "err", err)
|
if bufferScale <= 0 {
|
||||||
|
bufferScale = 1
|
||||||
}
|
}
|
||||||
|
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||||
}
|
}
|
||||||
|
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||||
if err := ls.wlSurface.Attach(wlBuffer, 0, 0); err != nil {
|
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||||
log.Warn("failed to attach buffer", "err", err)
|
_ = ls.wlSurface.Commit()
|
||||||
}
|
|
||||||
if err := ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH)); err != nil {
|
|
||||||
log.Warn("failed to damage surface", "err", err)
|
|
||||||
}
|
|
||||||
if err := ls.wlSurface.Commit(); err != nil {
|
|
||||||
log.Warn("failed to commit surface", "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ls.state.SwapBuffers()
|
ls.state.SwapBuffers()
|
||||||
}
|
}
|
||||||
@@ -596,17 +586,19 @@ func (p *Picker) setupInput() {
|
|||||||
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
p.seat.SetCapabilitiesHandler(func(e client.SeatCapabilitiesEvent) {
|
||||||
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
|
if e.Capabilities&uint32(client.SeatCapabilityPointer) != 0 && p.pointer == nil {
|
||||||
pointer, err := p.seat.GetPointer()
|
pointer, err := p.seat.GetPointer()
|
||||||
if err == nil {
|
if err != nil {
|
||||||
p.pointer = pointer
|
return
|
||||||
p.setupPointerHandlers()
|
|
||||||
}
|
}
|
||||||
|
p.pointer = pointer
|
||||||
|
p.setupPointerHandlers()
|
||||||
}
|
}
|
||||||
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
|
if e.Capabilities&uint32(client.SeatCapabilityKeyboard) != 0 && p.keyboard == nil {
|
||||||
keyboard, err := p.seat.GetKeyboard()
|
keyboard, err := p.seat.GetKeyboard()
|
||||||
if err == nil {
|
if err != nil {
|
||||||
p.keyboard = keyboard
|
return
|
||||||
p.setupKeyboardHandlers()
|
|
||||||
}
|
}
|
||||||
|
p.keyboard = keyboard
|
||||||
|
p.setupKeyboardHandlers()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -617,9 +609,14 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
log.Debug("failed to hide cursor", "err", err)
|
log.Debug("failed to hide cursor", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
p.activeSurface = nil
|
p.activeSurface = nil
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
for _, ls := range p.surfaces {
|
for _, ls := range p.surfaces {
|
||||||
if ls.wlSurface.ID() == e.Surface.ID() {
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
p.activeSurface = ls
|
p.activeSurface = ls
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -628,7 +625,6 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If surface was hidden, mark it as visible again
|
|
||||||
if p.activeSurface.hidden {
|
if p.activeSurface.hidden {
|
||||||
p.activeSurface.hidden = false
|
p.activeSurface.hidden = false
|
||||||
}
|
}
|
||||||
@@ -638,8 +634,12 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||||
|
if e.Surface == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
surfaceID := e.Surface.ID()
|
||||||
for _, ls := range p.surfaces {
|
for _, ls := range p.surfaces {
|
||||||
if ls.wlSurface.ID() == e.Surface.ID() {
|
if ls.wlSurface.ID() == surfaceID {
|
||||||
p.hideSurface(ls)
|
p.hideSurface(ls)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -672,6 +672,15 @@ func (p *Picker) setupKeyboardHandlers() {
|
|||||||
|
|
||||||
func (p *Picker) cleanup() {
|
func (p *Picker) cleanup() {
|
||||||
for _, ls := range p.surfaces {
|
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 {
|
if ls.wlBuffer != nil {
|
||||||
ls.wlBuffer.Destroy()
|
ls.wlBuffer.Destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,40 @@
|
|||||||
package colorpicker
|
package colorpicker
|
||||||
|
|
||||||
import (
|
import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
type ShmBuffer = shm.Buffer
|
||||||
)
|
|
||||||
|
|
||||||
type ShmBuffer struct {
|
|
||||||
fd int
|
|
||||||
data []byte
|
|
||||||
size int
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
Stride int
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
size := stride * height
|
return shm.CreateBuffer(width, height, stride)
|
||||||
|
|
||||||
fd, err := unix.MemfdCreate("dms-colorpicker", 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create memfd: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unix.Ftruncate(fd, int64(size)); err != nil {
|
|
||||||
unix.Close(fd)
|
|
||||||
return nil, fmt.Errorf("ftruncate failed: %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 failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ShmBuffer{
|
|
||||||
fd: fd,
|
|
||||||
data: data,
|
|
||||||
size: size,
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
Stride: stride,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShmBuffer) Fd() int {
|
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
|
||||||
return s.fd
|
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShmBuffer) Size() int {
|
func GetPixelColorWithFormat(buf *ShmBuffer, x, y int, format PixelFormat) Color {
|
||||||
return s.size
|
if x < 0 || x >= buf.Width || y < 0 || y >= buf.Height {
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ShmBuffer) Data() []byte {
|
|
||||||
return s.data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ShmBuffer) GetPixel(x, y int) Color {
|
|
||||||
if x < 0 || x >= s.Width || y < 0 || y >= s.Height {
|
|
||||||
return Color{}
|
return Color{}
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := y*s.Stride + x*4
|
data := buf.Data()
|
||||||
|
offset := y*buf.Stride + x*4
|
||||||
if offset+3 >= len(s.data) {
|
if offset+3 >= len(data) {
|
||||||
return Color{}
|
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{
|
return Color{
|
||||||
B: s.data[offset],
|
B: data[offset],
|
||||||
G: s.data[offset+1],
|
G: data[offset+1],
|
||||||
R: s.data[offset+2],
|
R: data[offset+2],
|
||||||
A: s.data[offset+3],
|
A: data[offset+3],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShmBuffer) Close() error {
|
|
||||||
var firstErr error
|
|
||||||
if s.data != nil {
|
|
||||||
if err := unix.Munmap(s.data); err != nil && firstErr == nil {
|
|
||||||
firstErr = fmt.Errorf("munmap failed: %w", err)
|
|
||||||
}
|
|
||||||
s.data = nil
|
|
||||||
}
|
|
||||||
if s.fd >= 0 {
|
|
||||||
if err := unix.Close(s.fd); err != nil && firstErr == nil {
|
|
||||||
firstErr = fmt.Errorf("close fd failed: %w", err)
|
|
||||||
}
|
|
||||||
s.fd = -1
|
|
||||||
}
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PixelFormat uint32
|
type PixelFormat = shm.PixelFormat
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FormatARGB8888 PixelFormat = 0
|
FormatARGB8888 = shm.FormatARGB8888
|
||||||
FormatXRGB8888 PixelFormat = 1
|
FormatXRGB8888 = shm.FormatXRGB8888
|
||||||
FormatABGR8888 PixelFormat = 0x34324241
|
FormatABGR8888 = shm.FormatABGR8888
|
||||||
FormatXBGR8888 PixelFormat = 0x34324258
|
FormatXBGR8888 = shm.FormatXBGR8888
|
||||||
)
|
)
|
||||||
|
|
||||||
type SurfaceState struct {
|
type SurfaceState struct {
|
||||||
@@ -98,6 +100,12 @@ func (s *SurfaceState) ScreenBuffer() *ShmBuffer {
|
|||||||
return s.screenBuf
|
return s.screenBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SurfaceState) ScreenFormat() PixelFormat {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.screenFormat
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
|
func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.yInverted = (flags & 1) != 0
|
s.yInverted = (flags & 1) != 0
|
||||||
@@ -253,7 +261,7 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(dst.data, s.screenBuf.data)
|
dst.CopyFrom(s.screenBuf)
|
||||||
|
|
||||||
px := int(math.Round(float64(s.pointerX) * s.scaleX))
|
px := int(math.Round(float64(s.pointerX) * s.scaleX))
|
||||||
py := int(math.Round(float64(s.pointerY) * s.scaleY))
|
py := int(math.Round(float64(s.pointerY) * s.scaleY))
|
||||||
@@ -261,15 +269,20 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
px = clamp(px, 0, dst.Width-1)
|
px = clamp(px, 0, dst.Width-1)
|
||||||
py = clamp(py, 0, dst.Height-1)
|
py = clamp(py, 0, dst.Height-1)
|
||||||
|
|
||||||
picked := s.screenBuf.GetPixel(px, py)
|
sampleY := py
|
||||||
|
if s.yInverted {
|
||||||
|
sampleY = s.screenBuf.Height - 1 - py
|
||||||
|
}
|
||||||
|
|
||||||
drawMagnifier(
|
picked := GetPixelColorWithFormat(s.screenBuf, px, sampleY, s.screenFormat)
|
||||||
dst.data, dst.Stride, dst.Width, dst.Height,
|
|
||||||
s.screenBuf.data, s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
drawMagnifierWithInversion(
|
||||||
px, py, picked,
|
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
||||||
|
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
||||||
|
px, py, picked, s.yInverted,
|
||||||
)
|
)
|
||||||
|
|
||||||
drawColorPreview(dst.data, dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
|
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
|
||||||
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
@@ -289,7 +302,7 @@ func (s *SurfaceState) RedrawScreenOnly() *ShmBuffer {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(dst.data, s.screenBuf.data)
|
dst.CopyFrom(s.screenBuf)
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +324,7 @@ func (s *SurfaceState) PickColor() (Color, bool) {
|
|||||||
sy = s.screenBuf.Height - 1 - sy
|
sy = s.screenBuf.Height - 1 - sy
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.screenBuf.GetPixel(sx, sy), true
|
return GetPixelColorWithFormat(s.screenBuf, sx, sy, s.screenFormat), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) Destroy() {
|
func (s *SurfaceState) Destroy() {
|
||||||
@@ -371,11 +384,12 @@ func blendColors(bg, fg Color, alpha float64) Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawMagnifier(
|
func drawMagnifierWithInversion(
|
||||||
dst []byte, dstStride, dstW, dstH int,
|
dst []byte, dstStride, dstW, dstH int,
|
||||||
src []byte, srcStride, srcW, srcH int,
|
src []byte, srcStride, srcW, srcH int,
|
||||||
cx, cy int,
|
cx, cy int,
|
||||||
borderColor Color,
|
borderColor Color,
|
||||||
|
yInverted bool,
|
||||||
) {
|
) {
|
||||||
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
|
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
|
||||||
return
|
return
|
||||||
@@ -431,10 +445,11 @@ func drawMagnifier(
|
|||||||
finalColor = blendColors(bgColor, borderColor, alpha)
|
finalColor = blendColors(bgColor, borderColor, alpha)
|
||||||
|
|
||||||
case dist > innerRadius:
|
case dist > innerRadius:
|
||||||
if dist > outerRadiusF-aaWidth {
|
switch {
|
||||||
|
case dist > outerRadiusF-aaWidth:
|
||||||
alpha := clampF((outerRadiusF-dist)/aaWidth, 0, 1)
|
alpha := clampF((outerRadiusF-dist)/aaWidth, 0, 1)
|
||||||
finalColor = blendColors(borderColor, borderColor, alpha)
|
finalColor = blendColors(borderColor, borderColor, alpha)
|
||||||
} else if dist < innerRadius+aaWidth {
|
case dist < innerRadius+aaWidth:
|
||||||
alpha := clampF((dist-innerRadius)/aaWidth, 0, 1)
|
alpha := clampF((dist-innerRadius)/aaWidth, 0, 1)
|
||||||
fx := float64(dx) / zoom
|
fx := float64(dx) / zoom
|
||||||
fy := float64(dy) / zoom
|
fy := float64(dy) / zoom
|
||||||
@@ -442,6 +457,9 @@ func drawMagnifier(
|
|||||||
sy := cy + int(math.Round(fy))
|
sy := cy + int(math.Round(fy))
|
||||||
sx = clamp(sx, 0, srcW-1)
|
sx = clamp(sx, 0, srcW-1)
|
||||||
sy = clamp(sy, 0, srcH-1)
|
sy = clamp(sy, 0, srcH-1)
|
||||||
|
if yInverted {
|
||||||
|
sy = srcH - 1 - sy
|
||||||
|
}
|
||||||
srcOff := sy*srcStride + sx*4
|
srcOff := sy*srcStride + sx*4
|
||||||
if srcOff+4 <= len(src) {
|
if srcOff+4 <= len(src) {
|
||||||
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
||||||
@@ -449,7 +467,7 @@ func drawMagnifier(
|
|||||||
} else {
|
} else {
|
||||||
finalColor = borderColor
|
finalColor = borderColor
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
finalColor = borderColor
|
finalColor = borderColor
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +478,9 @@ func drawMagnifier(
|
|||||||
sy := cy + int(math.Round(fy))
|
sy := cy + int(math.Round(fy))
|
||||||
sx = clamp(sx, 0, srcW-1)
|
sx = clamp(sx, 0, srcW-1)
|
||||||
sy = clamp(sy, 0, srcH-1)
|
sy = clamp(sy, 0, srcH-1)
|
||||||
|
if yInverted {
|
||||||
|
sy = srcH - 1 - sy
|
||||||
|
}
|
||||||
srcOff := sy*srcStride + sx*4
|
srcOff := sy*srcStride + sx*4
|
||||||
if srcOff+4 <= len(src) {
|
if srcOff+4 <= len(src) {
|
||||||
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
||||||
|
|||||||
@@ -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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -487,16 +487,14 @@ func TestNiriConfigStructure(t *testing.T) {
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -89,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") {
|
||||||
@@ -149,7 +134,6 @@ func (d *DebianDistribution) 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},
|
||||||
|
|
||||||
// DMS packages from OBS with variant support
|
// DMS packages from OBS with variant support
|
||||||
@@ -158,9 +142,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
// Keep ghostty as manual (no OBS package yet)
|
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if wm == deps.WindowManagerNiri {
|
if wm == deps.WindowManagerNiri {
|
||||||
@@ -351,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,
|
||||||
@@ -664,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
|
||||||
@@ -697,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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -101,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()
|
||||||
@@ -134,7 +119,6 @@ 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},
|
||||||
|
|
||||||
@@ -148,10 +132,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
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:
|
||||||
// Niri stable has native package support on openSUSE
|
// Niri stable has native package support on openSUSE
|
||||||
@@ -391,6 +372,15 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting post-installation configuration...",
|
LogOutput: "Starting post-installation configuration...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Complete
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseComplete,
|
Phase: PhaseComplete,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -101,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") {
|
||||||
@@ -161,7 +144,6 @@ func (u *UbuntuDistribution) 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},
|
||||||
|
|
||||||
// DMS packages from PPAs
|
// DMS packages from PPAs
|
||||||
@@ -170,19 +152,14 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
// Keep ghostty as manual (no PPA available)
|
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
||||||
niriVariant := variants["niri"]
|
niriVariant := variants["niri"]
|
||||||
@@ -375,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,
|
||||||
@@ -577,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":
|
||||||
@@ -594,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)
|
||||||
@@ -661,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
|
||||||
@@ -742,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
|
||||||
@@ -775,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)
|
||||||
|
|||||||
@@ -371,6 +371,18 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
|||||||
|
|
||||||
node.SetName(parts[0])
|
node.SetName(parts[0])
|
||||||
for _, arg := range parts[1:] {
|
for _, arg := range parts[1:] {
|
||||||
|
if strings.Contains(arg, "=") {
|
||||||
|
kv := strings.SplitN(arg, "=", 2)
|
||||||
|
switch kv[1] {
|
||||||
|
case "true":
|
||||||
|
node.AddProperty(kv[0], true, "")
|
||||||
|
case "false":
|
||||||
|
node.AddProperty(kv[0], false, "")
|
||||||
|
default:
|
||||||
|
node.AddProperty(kv[0], kv[1], "")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
node.AddArgument(arg, "")
|
node.AddArgument(arg, "")
|
||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
@@ -379,7 +391,7 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
|||||||
func (n *NiriProvider) parseActionParts(action string) []string {
|
func (n *NiriProvider) parseActionParts(action string) []string {
|
||||||
var parts []string
|
var parts []string
|
||||||
var current strings.Builder
|
var current strings.Builder
|
||||||
var inQuote, escaped bool
|
var inQuote, escaped, wasQuoted bool
|
||||||
|
|
||||||
for _, r := range action {
|
for _, r := range action {
|
||||||
switch {
|
switch {
|
||||||
@@ -389,17 +401,19 @@ func (n *NiriProvider) parseActionParts(action string) []string {
|
|||||||
case r == '\\':
|
case r == '\\':
|
||||||
escaped = true
|
escaped = true
|
||||||
case r == '"':
|
case r == '"':
|
||||||
|
wasQuoted = true
|
||||||
inQuote = !inQuote
|
inQuote = !inQuote
|
||||||
case r == ' ' && !inQuote:
|
case r == ' ' && !inQuote:
|
||||||
if current.Len() > 0 {
|
if current.Len() > 0 || wasQuoted {
|
||||||
parts = append(parts, current.String())
|
parts = append(parts, current.String())
|
||||||
current.Reset()
|
current.Reset()
|
||||||
|
wasQuoted = false
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
current.WriteRune(r)
|
current.WriteRune(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if current.Len() > 0 {
|
if current.Len() > 0 || wasQuoted {
|
||||||
parts = append(parts, current.String())
|
parts = append(parts, current.String())
|
||||||
}
|
}
|
||||||
return parts
|
return parts
|
||||||
@@ -508,6 +522,10 @@ func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, in
|
|||||||
sb.WriteString(" ")
|
sb.WriteString(" ")
|
||||||
n.writeArg(sb, arg.ValueString(), forceQuote)
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -602,8 +602,24 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
|||||||
for _, kb := range result.Section.Keybinds {
|
for _, kb := range result.Section.Keybinds {
|
||||||
switch kb.Action {
|
switch kb.Action {
|
||||||
case "move-column-to-workspace":
|
case "move-column-to-workspace":
|
||||||
if len(kb.Args) != 1 {
|
if len(kb.Args) != 2 {
|
||||||
t.Errorf("move-column-to-workspace should have 1 arg, got %d", len(kb.Args))
|
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":
|
case "next-window":
|
||||||
if kb.Key != "Tab" {
|
if kb.Key != "Tab" {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func NewZwpKeyboardShortcutsInhibitManagerV1(ctx *client.Context) *ZwpKeyboardSh
|
|||||||
//
|
//
|
||||||
// Destroy the keyboard shortcuts inhibitor manager.
|
// Destroy the keyboard shortcuts inhibitor manager.
|
||||||
func (i *ZwpKeyboardShortcutsInhibitManagerV1) Destroy() error {
|
func (i *ZwpKeyboardShortcutsInhibitManagerV1) 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
|
||||||
@@ -218,7 +218,7 @@ func NewZwpKeyboardShortcutsInhibitorV1(ctx *client.Context) *ZwpKeyboardShortcu
|
|||||||
//
|
//
|
||||||
// Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
// Remove the keyboard shortcuts inhibitor from the associated wl_surface.
|
||||||
func (i *ZwpKeyboardShortcutsInhibitorV1) Destroy() error {
|
func (i *ZwpKeyboardShortcutsInhibitorV1) 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func (i *ZwlrLayerShellV1) GetLayerSurface(surface *client.Surface, output *clie
|
|||||||
// object any more. Objects that have been created through this instance
|
// object any more. Objects that have been created through this instance
|
||||||
// are not affected.
|
// are not affected.
|
||||||
func (i *ZwlrLayerShellV1) Destroy() error {
|
func (i *ZwlrLayerShellV1) 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
|
||||||
@@ -509,7 +509,7 @@ func (i *ZwlrLayerSurfaceV1) AckConfigure(serial uint32) error {
|
|||||||
//
|
//
|
||||||
// This request destroys the layer surface.
|
// This request destroys the layer surface.
|
||||||
func (i *ZwlrLayerSurfaceV1) Destroy() error {
|
func (i *ZwlrLayerSurfaceV1) Destroy() error {
|
||||||
defer i.Context().Unregister(i)
|
defer i.MarkZombie()
|
||||||
const opcode = 7
|
const opcode = 7
|
||||||
const _reqBufLen = 8
|
const _reqBufLen = 8
|
||||||
var _reqBuf [_reqBufLen]byte
|
var _reqBuf [_reqBufLen]byte
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (i *ZwlrScreencopyManagerV1) CaptureOutputRegion(overlayCursor int32, outpu
|
|||||||
// 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 *ZwlrScreencopyManagerV1) Destroy() error {
|
func (i *ZwlrScreencopyManagerV1) 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
|
||||||
@@ -219,7 +219,7 @@ func (i *ZwlrScreencopyFrameV1) Copy(buffer *client.Buffer) error {
|
|||||||
//
|
//
|
||||||
// Destroys the frame. This request can be sent at any time by the client.
|
// Destroys the frame. This request can be sent at any time by the client.
|
||||||
func (i *ZwlrScreencopyFrameV1) Destroy() error {
|
func (i *ZwlrScreencopyFrameV1) 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
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func NewWpViewporter(ctx *client.Context) *WpViewporter {
|
|||||||
// protocol object anymore. This does not affect any other objects,
|
// protocol object anymore. This does not affect any other objects,
|
||||||
// wp_viewport objects included.
|
// wp_viewport objects included.
|
||||||
func (i *WpViewporter) Destroy() error {
|
func (i *WpViewporter) 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
|
||||||
@@ -267,7 +267,7 @@ func NewWpViewport(ctx *client.Context) *WpViewport {
|
|||||||
// The associated wl_surface's crop and scale state is removed.
|
// The associated wl_surface's crop and scale state is removed.
|
||||||
// The change is applied on the next wl_surface.commit.
|
// The change is applied on the next wl_surface.commit.
|
||||||
func (i *WpViewport) Destroy() error {
|
func (i *WpViewport) 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,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")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
-4
@@ -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).";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+193
-164
@@ -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]}"
|
||||||
if [[ -z "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR/debian" ]]; then
|
PPA_NUM="${BASH_REMATCH[5]}"
|
||||||
echo " Error: Source directory with debian/ not found for version increment"
|
|
||||||
exit 1
|
# Check if old DSC has ppa suffix even if changelog doesn't
|
||||||
fi
|
if [[ -z "$PPA_NUM" ]] && [[ "$OLD_DSC_VERSION" =~ ppa([0-9]+)$ ]]; then
|
||||||
|
OLD_PPA_NUM="${BASH_REMATCH[1]}"
|
||||||
SOURCE_CHANGELOG="$SOURCE_DIR/debian/changelog"
|
NEW_PPA_NUM=$((OLD_PPA_NUM + 1))
|
||||||
if [[ ! -f "$SOURCE_CHANGELOG" ]]; then
|
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
|
||||||
echo " Error: Changelog not found in source directory: $SOURCE_CHANGELOG"
|
echo " Incrementing PPA number from old DSC: $OLD_DSC_VERSION -> $NEW_VERSION"
|
||||||
exit 1
|
elif [[ -n "$PPA_NUM" ]]; then
|
||||||
fi
|
NEW_PPA_NUM=$((PPA_NUM + 1))
|
||||||
|
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa${NEW_PPA_NUM}"
|
||||||
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
echo " Incrementing PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
else
|
||||||
{
|
NEW_VERSION="${BASE_VERSION}+git${GIT_NUM}${GIT_HASH}ppa1"
|
||||||
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
echo " Adding PPA number: $CHANGELOG_VERSION -> $NEW_VERSION"
|
||||||
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"
|
if [[ -z "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR" ]] || [[ ! -d "$SOURCE_DIR/debian" ]]; then
|
||||||
rm -f "$TEMP_CHANGELOG"
|
echo " Error: Source directory with debian/ not found for version increment"
|
||||||
|
exit 1
|
||||||
CHANGELOG_VERSION="$NEW_VERSION"
|
|
||||||
VERSION="$NEW_VERSION"
|
|
||||||
COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
SOURCE_CHANGELOG="$SOURCE_DIR/debian/changelog"
|
||||||
if [[ "$PACKAGE" == "dms" ]] && [[ -f "$WORK_DIR/dms-source.tar.gz" ]]; then
|
if [[ ! -f "$SOURCE_CHANGELOG" ]]; then
|
||||||
echo " Recreating dms-source.tar.gz with new directory name for incremented version"
|
echo " Error: Changelog not found in source directory: $SOURCE_CHANGELOG"
|
||||||
EXPECTED_SOURCE_DIR="DankMaterialShell-${NEW_VERSION}"
|
exit 1
|
||||||
TEMP_SOURCE_DIR=$(mktemp -d)
|
fi
|
||||||
cd "$TEMP_SOURCE_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
|
REPO_CHANGELOG="$REPO_ROOT/distro/debian/$PACKAGE/debian/changelog"
|
||||||
EXTRACTED=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
if [[ -n "$EXTRACTED" ]] && [[ "$EXTRACTED" != "./$EXPECTED_SOURCE_DIR" ]]; then
|
{
|
||||||
echo " Renaming $EXTRACTED to $EXPECTED_SOURCE_DIR"
|
echo "$PACKAGE ($NEW_VERSION) unstable; urgency=medium"
|
||||||
mv "$EXTRACTED" "$EXPECTED_SOURCE_DIR"
|
echo ""
|
||||||
rm -f "$WORK_DIR/dms-source.tar.gz"
|
echo " * Rebuild to fix repository metadata issues"
|
||||||
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 ""
|
||||||
ROOT_DIR=$(tar -tf "$WORK_DIR/dms-source.tar.gz" | head -1 | cut -d/ -f1)
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
|
||||||
if [[ "$ROOT_DIR" != "$EXPECTED_SOURCE_DIR" ]]; then
|
echo ""
|
||||||
echo " Error: Recreated tarball has wrong root directory: $ROOT_DIR (expected $EXPECTED_SOURCE_DIR)"
|
if [[ -f "$REPO_CHANGELOG" ]]; then
|
||||||
exit 1
|
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
|
||||||
|
} > "$TEMP_CHANGELOG"
|
||||||
|
cp "$TEMP_CHANGELOG" "$SOURCE_CHANGELOG"
|
||||||
|
rm -f "$TEMP_CHANGELOG"
|
||||||
|
|
||||||
|
CHANGELOG_VERSION="$NEW_VERSION"
|
||||||
|
VERSION="$NEW_VERSION"
|
||||||
|
COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
|
||||||
|
|
||||||
|
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
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$PACKAGE" == "dms" ]] && [[ -f "$WORK_DIR/dms-source.tar.gz" ]]; then
|
||||||
|
echo " Recreating dms-source.tar.gz with new directory name for incremented version"
|
||||||
|
EXPECTED_SOURCE_DIR="DankMaterialShell-${NEW_VERSION}"
|
||||||
|
TEMP_SOURCE_DIR=$(mktemp -d)
|
||||||
|
cd "$TEMP_SOURCE_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
|
||||||
|
EXTRACTED=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
|
||||||
|
if [[ -n "$EXTRACTED" ]] && [[ "$EXTRACTED" != "./$EXPECTED_SOURCE_DIR" ]]; then
|
||||||
|
echo " Renaming $EXTRACTED to $EXPECTED_SOURCE_DIR"
|
||||||
|
mv "$EXTRACTED" "$EXPECTED_SOURCE_DIR"
|
||||||
|
rm -f "$WORK_DIR/dms-source.tar.gz"
|
||||||
|
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/dms-source.tar.gz" "$EXPECTED_SOURCE_DIR"
|
||||||
|
ROOT_DIR=$(tar -tf "$WORK_DIR/dms-source.tar.gz" | head -1 | cut -d/ -f1)
|
||||||
|
if [[ "$ROOT_DIR" != "$EXPECTED_SOURCE_DIR" ]]; then
|
||||||
|
echo " Error: Recreated tarball has wrong root directory: $ROOT_DIR (expected $EXPECTED_SOURCE_DIR)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
rm -rf "$TEMP_SOURCE_DIR"
|
||||||
fi
|
fi
|
||||||
cd "$REPO_ROOT"
|
|
||||||
rm -rf "$TEMP_SOURCE_DIR"
|
echo " Recreating tarball with new version: $COMBINED_TARBALL"
|
||||||
fi
|
if [[ -n "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR/debian" ]]; then
|
||||||
|
if [[ "$PACKAGE" == "dms" ]]; then
|
||||||
echo " Recreating tarball with new version: $COMBINED_TARBALL"
|
cd "$(dirname "$SOURCE_DIR")"
|
||||||
if [[ -n "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR" ]] && [[ -d "$SOURCE_DIR/debian" ]]; then
|
CURRENT_DIR=$(basename "$SOURCE_DIR")
|
||||||
if [[ "$PACKAGE" == "dms" ]]; then
|
EXPECTED_DIR="DankMaterialShell-${NEW_VERSION}"
|
||||||
cd "$(dirname "$SOURCE_DIR")"
|
if [[ "$CURRENT_DIR" != "$EXPECTED_DIR" ]]; then
|
||||||
CURRENT_DIR=$(basename "$SOURCE_DIR")
|
echo " Renaming directory from $CURRENT_DIR to $EXPECTED_DIR to match debian/rules"
|
||||||
EXPECTED_DIR="DankMaterialShell-${NEW_VERSION}"
|
if [[ -d "$CURRENT_DIR" ]]; then
|
||||||
if [[ "$CURRENT_DIR" != "$EXPECTED_DIR" ]]; then
|
mv "$CURRENT_DIR" "$EXPECTED_DIR"
|
||||||
echo " Renaming directory from $CURRENT_DIR to $EXPECTED_DIR to match debian/rules"
|
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
||||||
if [[ -d "$CURRENT_DIR" ]]; then
|
|
||||||
mv "$CURRENT_DIR" "$EXPECTED_DIR"
|
|
||||||
SOURCE_DIR="$(pwd)/$EXPECTED_DIR"
|
|
||||||
else
|
|
||||||
echo " Warning: Source directory $CURRENT_DIR not found, extracting from existing tarball"
|
|
||||||
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
|
|
||||||
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
|
||||||
|
|
||||||
|
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
|
fi
|
||||||
cd "$(dirname "$SOURCE_DIR")"
|
else
|
||||||
TARBALL_BASE=$(basename "$SOURCE_DIR")
|
echo "==> Detected same version. Not a manual run, skipping Debian version increment."
|
||||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
|
echo "✅ No changes needed for Debian. Exiting."
|
||||||
cd "$WORK_DIR"
|
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"
|
||||||
|
|||||||
Generated
+29
-7
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,14 +197,26 @@ const ACTION_ARGS = {
|
|||||||
{ name: "focus", type: "bool", label: "Follow focus", default: false }
|
{ 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": {
|
"screenshot": {
|
||||||
args: [{ name: "opts", type: "screenshot", label: "Options" }]
|
args: [{ name: "show-pointer", type: "bool", label: "Show pointer" }]
|
||||||
},
|
},
|
||||||
"screenshot-screen": {
|
"screenshot-screen": {
|
||||||
args: [{ name: "opts", type: "screenshot", label: "Options" }]
|
args: [
|
||||||
|
{ name: "show-pointer", type: "bool", label: "Show pointer" },
|
||||||
|
{ name: "write-to-disk", type: "bool", label: "Save to disk" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"screenshot-window": {
|
"screenshot-window": {
|
||||||
args: [{ name: "opts", type: "screenshot", label: "Options" }]
|
args: [
|
||||||
|
{ name: "show-pointer", type: "bool", label: "Show pointer" },
|
||||||
|
{ name: "write-to-disk", type: "bool", label: "Save to disk" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,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;
|
||||||
|
|
||||||
@@ -337,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) {
|
||||||
@@ -404,10 +418,10 @@ function parseCompositorActionArgs(action) {
|
|||||||
if (!ACTION_ARGS[base])
|
if (!ACTION_ARGS[base])
|
||||||
return { base: action, args: {} };
|
return { base: action, args: {} };
|
||||||
|
|
||||||
var argConfig = ACTION_ARGS[base];
|
|
||||||
var argParts = parts.slice(1);
|
var argParts = parts.slice(1);
|
||||||
|
|
||||||
if (base === "move-column-to-workspace") {
|
switch (base) {
|
||||||
|
case "move-column-to-workspace":
|
||||||
for (var i = 0; i < argParts.length; i++) {
|
for (var i = 0; i < argParts.length; i++) {
|
||||||
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
|
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
|
||||||
args.focus = argParts[i] === "focus=true";
|
args.focus = argParts[i] === "focus=true";
|
||||||
@@ -415,14 +429,24 @@ function parseCompositorActionArgs(action) {
|
|||||||
args.index = argParts[i];
|
args.index = argParts[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (base.startsWith("screenshot")) {
|
break;
|
||||||
args.opts = {};
|
case "move-column-to-workspace-down":
|
||||||
for (var j = 0; j < argParts.length; j += 2) {
|
case "move-column-to-workspace-up":
|
||||||
if (j + 1 < argParts.length)
|
for (var k = 0; k < argParts.length; k++) {
|
||||||
args.opts[argParts[j]] = argParts[j + 1];
|
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(" ");
|
||||||
}
|
}
|
||||||
} else if (argParts.length > 0) {
|
|
||||||
args.value = argParts.join(" ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { base: base, args: args };
|
return { base: base, args: args };
|
||||||
@@ -437,24 +461,29 @@ function buildCompositorAction(base, args) {
|
|||||||
if (!args || Object.keys(args).length === 0)
|
if (!args || Object.keys(args).length === 0)
|
||||||
return base;
|
return base;
|
||||||
|
|
||||||
if (base === "move-column-to-workspace") {
|
switch (base) {
|
||||||
|
case "move-column-to-workspace":
|
||||||
if (args.index)
|
if (args.index)
|
||||||
parts.push(args.index);
|
parts.push(args.index);
|
||||||
if (args.focus === true)
|
if (args.focus === false)
|
||||||
parts.push("focus=true");
|
|
||||||
else if (args.focus === false)
|
|
||||||
parts.push("focus=false");
|
parts.push("focus=false");
|
||||||
} else if (base.startsWith("screenshot") && args.opts) {
|
break;
|
||||||
for (var key in args.opts) {
|
case "move-column-to-workspace-down":
|
||||||
if (args.opts[key] !== undefined && args.opts[key] !== "") {
|
case "move-column-to-workspace-up":
|
||||||
parts.push(key);
|
if (args.focus === false)
|
||||||
parts.push(args.opts[key]);
|
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);
|
||||||
}
|
}
|
||||||
} else if (args.value) {
|
|
||||||
parts.push(args.value);
|
|
||||||
} else if (args.index) {
|
|
||||||
parts.push(args.index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -295,7 +295,7 @@ 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 lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
@@ -318,7 +318,7 @@ Singleton {
|
|||||||
property bool osdAudioOutputEnabled: 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
|
||||||
|
|||||||
+27
-10
@@ -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 },
|
||||||
@@ -194,7 +194,7 @@ 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" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
@@ -217,7 +217,7 @@ var SPEC = {
|
|||||||
osdAudioOutputEnabled: { def: true },
|
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 },
|
||||||
|
|||||||
@@ -787,12 +787,18 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
readonly property int remainingSeconds: Math.ceil(SettingsData.powerActionHoldDuration * (1 - root.holdProgress))
|
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
|
||||||
|
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
||||||
text: {
|
text: {
|
||||||
if (root.showHoldHint)
|
if (root.showHoldHint)
|
||||||
return I18n.tr("Hold longer to confirm");
|
return I18n.tr("Hold longer to confirm");
|
||||||
if (root.holdProgress > 0)
|
if (root.holdProgress > 0) {
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(remainingSeconds);
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
|
||||||
|
return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
|
||||||
|
}
|
||||||
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
|
id: volumeSlider
|
||||||
|
|
||||||
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -63,7 +65,6 @@ Row {
|
|||||||
enabled: defaultSink !== null
|
enabled: defaultSink !== null
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
|
|
||||||
showValue: true
|
showValue: true
|
||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
@@ -81,4 +82,11 @@ Row {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: volumeSlider
|
||||||
|
property: "value"
|
||||||
|
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
|
||||||
|
when: !volumeSlider.isDragging
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ Item {
|
|||||||
id: barBorderShape
|
id: barBorderShape
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
preferredRendererType: Shape.CurveRenderer
|
preferredRendererType: Shape.CurveRenderer
|
||||||
visible: (barConfig?.borderEnabled ?? false) && !barWindow.hasMaximizedToplevel
|
visible: barConfig?.borderEnabled ?? false
|
||||||
|
|
||||||
ShapePath {
|
ShapePath {
|
||||||
fillColor: "transparent"
|
fillColor: "transparent"
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ Item {
|
|||||||
property var itemData: modelData
|
property var itemData: modelData
|
||||||
readonly property real itemSpacing: root.widgetSpacing
|
readonly property real itemSpacing: root.widgetSpacing
|
||||||
|
|
||||||
width: widgetLoader.item ? widgetLoader.item.width : 0
|
width: root.isVertical ? root.width : (widgetLoader.item ? widgetLoader.item.width : 0)
|
||||||
height: widgetLoader.item ? widgetLoader.item.height : 0
|
height: widgetLoader.item ? widgetLoader.item.height : 0
|
||||||
|
|
||||||
readonly property bool active: widgetLoader.active
|
readonly property bool active: widgetLoader.active
|
||||||
|
|||||||
@@ -302,7 +302,8 @@ Item {
|
|||||||
"vpn": vpnComponent,
|
"vpn": vpnComponent,
|
||||||
"notepadButton": notepadButtonComponent,
|
"notepadButton": notepadButtonComponent,
|
||||||
"colorPicker": colorPickerComponent,
|
"colorPicker": colorPickerComponent,
|
||||||
"systemUpdate": systemUpdateComponent
|
"systemUpdate": systemUpdateComponent,
|
||||||
|
"powerMenuButton": powerMenuButtonComponent
|
||||||
};
|
};
|
||||||
|
|
||||||
let pluginMap = PluginService.getWidgetComponents();
|
let pluginMap = PluginService.getWidgetComponents();
|
||||||
@@ -314,36 +315,37 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property var allComponents: ({
|
readonly property var allComponents: ({
|
||||||
"launcherButtonComponent": launcherButtonComponent,
|
"launcherButtonComponent": launcherButtonComponent,
|
||||||
"workspaceSwitcherComponent": workspaceSwitcherComponent,
|
"workspaceSwitcherComponent": workspaceSwitcherComponent,
|
||||||
"focusedWindowComponent": focusedWindowComponent,
|
"focusedWindowComponent": focusedWindowComponent,
|
||||||
"runningAppsComponent": runningAppsComponent,
|
"runningAppsComponent": runningAppsComponent,
|
||||||
"clockComponent": clockComponent,
|
"clockComponent": clockComponent,
|
||||||
"mediaComponent": mediaComponent,
|
"mediaComponent": mediaComponent,
|
||||||
"weatherComponent": weatherComponent,
|
"weatherComponent": weatherComponent,
|
||||||
"systemTrayComponent": systemTrayComponent,
|
"systemTrayComponent": systemTrayComponent,
|
||||||
"privacyIndicatorComponent": privacyIndicatorComponent,
|
"privacyIndicatorComponent": privacyIndicatorComponent,
|
||||||
"clipboardComponent": clipboardComponent,
|
"clipboardComponent": clipboardComponent,
|
||||||
"cpuUsageComponent": cpuUsageComponent,
|
"cpuUsageComponent": cpuUsageComponent,
|
||||||
"memUsageComponent": memUsageComponent,
|
"memUsageComponent": memUsageComponent,
|
||||||
"diskUsageComponent": diskUsageComponent,
|
"diskUsageComponent": diskUsageComponent,
|
||||||
"cpuTempComponent": cpuTempComponent,
|
"cpuTempComponent": cpuTempComponent,
|
||||||
"gpuTempComponent": gpuTempComponent,
|
"gpuTempComponent": gpuTempComponent,
|
||||||
"notificationButtonComponent": notificationButtonComponent,
|
"notificationButtonComponent": notificationButtonComponent,
|
||||||
"batteryComponent": batteryComponent,
|
"batteryComponent": batteryComponent,
|
||||||
"layoutComponent": layoutComponent,
|
"layoutComponent": layoutComponent,
|
||||||
"controlCenterButtonComponent": controlCenterButtonComponent,
|
"controlCenterButtonComponent": controlCenterButtonComponent,
|
||||||
"capsLockIndicatorComponent": capsLockIndicatorComponent,
|
"capsLockIndicatorComponent": capsLockIndicatorComponent,
|
||||||
"idleInhibitorComponent": idleInhibitorComponent,
|
"idleInhibitorComponent": idleInhibitorComponent,
|
||||||
"spacerComponent": spacerComponent,
|
"spacerComponent": spacerComponent,
|
||||||
"separatorComponent": separatorComponent,
|
"separatorComponent": separatorComponent,
|
||||||
"networkComponent": networkComponent,
|
"networkComponent": networkComponent,
|
||||||
"keyboardLayoutNameComponent": keyboardLayoutNameComponent,
|
"keyboardLayoutNameComponent": keyboardLayoutNameComponent,
|
||||||
"vpnComponent": vpnComponent,
|
"vpnComponent": vpnComponent,
|
||||||
"notepadButtonComponent": notepadButtonComponent,
|
"notepadButtonComponent": notepadButtonComponent,
|
||||||
"colorPickerComponent": colorPickerComponent,
|
"colorPickerComponent": colorPickerComponent,
|
||||||
"systemUpdateComponent": systemUpdateComponent
|
"systemUpdateComponent": systemUpdateComponent,
|
||||||
})
|
"powerMenuButtonComponent": powerMenuButtonComponent
|
||||||
|
})
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: stackContainer
|
id: stackContainer
|
||||||
@@ -532,7 +534,27 @@ Item {
|
|||||||
section: topBarContent.getWidgetSection(parent)
|
section: topBarContent.getWidgetSection(parent)
|
||||||
parentScreen: barWindow.screen
|
parentScreen: barWindow.screen
|
||||||
onClicked: {
|
onClicked: {
|
||||||
clipboardHistoryModalPopup.toggle();
|
clipboardHistoryModalPopup.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: powerMenuButtonComponent
|
||||||
|
|
||||||
|
PowerMenuButton {
|
||||||
|
widgetThickness: barWindow.widgetThickness
|
||||||
|
barThickness: barWindow.effectiveBarThickness
|
||||||
|
axis: barWindow.axis
|
||||||
|
section: topBarContent.getWidgetSection(parent)
|
||||||
|
parentScreen: barWindow.screen
|
||||||
|
onClicked: {
|
||||||
|
if (powerMenuModalLoader) {
|
||||||
|
powerMenuModalLoader.active = true
|
||||||
|
if (powerMenuModalLoader.item) {
|
||||||
|
powerMenuModalLoader.item.openCentered()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Loader {
|
|||||||
property bool isRightBarEdge: false
|
property bool isRightBarEdge: false
|
||||||
property bool isTopBarEdge: false
|
property bool isTopBarEdge: false
|
||||||
property bool isBottomBarEdge: false
|
property bool isBottomBarEdge: false
|
||||||
|
property string _registeredScreenName: ""
|
||||||
|
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
|
|
||||||
@@ -198,13 +199,16 @@ Loader {
|
|||||||
if (!hasPopout)
|
if (!hasPopout)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
BarWidgetService.registerWidget(widgetId, parentScreen.name, item);
|
_registeredScreenName = parentScreen.name;
|
||||||
|
BarWidgetService.registerWidget(widgetId, _registeredScreenName, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unregisterWidget() {
|
function unregisterWidget() {
|
||||||
if (!widgetId || !parentScreen?.name)
|
if (!widgetId || !_registeredScreenName)
|
||||||
return;
|
return;
|
||||||
BarWidgetService.unregisterWidget(widgetId, parentScreen.name);
|
|
||||||
|
BarWidgetService.unregisterWidget(widgetId, _registeredScreenName);
|
||||||
|
_registeredScreenName = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWidgetComponent(widgetId, components) {
|
function getWidgetComponent(widgetId, components) {
|
||||||
@@ -237,7 +241,8 @@ Loader {
|
|||||||
"notepadButton": components.notepadButtonComponent,
|
"notepadButton": components.notepadButtonComponent,
|
||||||
"colorPicker": components.colorPickerComponent,
|
"colorPicker": components.colorPickerComponent,
|
||||||
"systemUpdate": components.systemUpdateComponent,
|
"systemUpdate": components.systemUpdateComponent,
|
||||||
"layout": components.layoutComponent
|
"layout": components.layoutComponent,
|
||||||
|
"powerMenuButton": components.powerMenuButtonComponent
|
||||||
};
|
};
|
||||||
|
|
||||||
if (componentMap[widgetId]) {
|
if (componentMap[widgetId]) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "content_paste"
|
name: "content_paste"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: root.getNetworkIconName()
|
name: root.getNetworkIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.getNetworkIconColor()
|
color: root.getNetworkIconColor()
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||||
@@ -181,7 +181,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "vpn_lock"
|
name: "vpn_lock"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.outlineButton
|
color: NetworkService.vpnConnected ? Theme.primary : Theme.outlineButton
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||||
@@ -189,7 +189,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "bluetooth"
|
name: "bluetooth"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||||
@@ -205,7 +205,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: audioIconV
|
id: audioIconV
|
||||||
name: root.getVolumeIconName()
|
name: root.getVolumeIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: micIconV
|
id: micIconV
|
||||||
name: root.getMicIconName()
|
name: root.getMicIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.getMicIconColor()
|
color: root.getMicIconColor()
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: brightnessIconV
|
id: brightnessIconV
|
||||||
name: root.getBrightnessIconName()
|
name: root.getBrightnessIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.getBatteryIconColor()
|
color: root.getBatteryIconColor()
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
||||||
@@ -280,7 +280,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "print"
|
name: "print"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
||||||
@@ -288,7 +288,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "settings"
|
name: "settings"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
visible: root.hasNoVisibleIcons()
|
visible: root.hasNoVisibleIcons()
|
||||||
@@ -304,7 +304,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: networkIcon
|
id: networkIcon
|
||||||
name: root.getNetworkIconName()
|
name: root.getNetworkIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.getNetworkIconColor()
|
color: root.getNetworkIconColor()
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||||
@@ -313,7 +313,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: vpnIcon
|
id: vpnIcon
|
||||||
name: "vpn_lock"
|
name: "vpn_lock"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.outlineButton
|
color: NetworkService.vpnConnected ? Theme.primary : Theme.outlineButton
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||||
@@ -322,7 +322,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: bluetoothIcon
|
id: bluetoothIcon
|
||||||
name: "bluetooth"
|
name: "bluetooth"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||||
@@ -338,7 +338,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: audioIcon
|
id: audioIcon
|
||||||
name: root.getVolumeIconName()
|
name: root.getVolumeIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
@@ -364,7 +364,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: micIcon
|
id: micIcon
|
||||||
name: root.getMicIconName()
|
name: root.getMicIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.getMicIconColor()
|
color: root.getMicIconColor()
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: brightnessIcon
|
id: brightnessIcon
|
||||||
name: root.getBrightnessIconName()
|
name: root.getBrightnessIconName()
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: Theme.widgetIconColor
|
color: Theme.widgetIconColor
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
@@ -409,7 +409,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: batteryIcon
|
id: batteryIcon
|
||||||
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.getBatteryIconColor()
|
color: root.getBatteryIconColor()
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
||||||
@@ -418,7 +418,7 @@ BasePill {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
id: printerIcon
|
id: printerIcon
|
||||||
name: "print"
|
name: "print"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
||||||
@@ -426,7 +426,7 @@ BasePill {
|
|||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: "settings"
|
name: "settings"
|
||||||
size: Theme.barIconSize(root.barThickness)
|
size: Theme.barIconSize(root.barThickness, -4)
|
||||||
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.hasNoVisibleIcons()
|
visible: root.hasNoVisibleIcons()
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool isActive: false
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
|
||||||
|
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "power_settings_new"
|
||||||
|
size: Theme.barIconSize(root.barThickness)
|
||||||
|
color: Theme.widgetIconColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -541,6 +541,34 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWorkspaceIndex(modelData) {
|
||||||
|
let isPlaceholder;
|
||||||
|
if (root.useExtWorkspace) {
|
||||||
|
isPlaceholder = modelData?.hidden === true;
|
||||||
|
} else if (CompositorService.isHyprland) {
|
||||||
|
isPlaceholder = modelData?.id === -1;
|
||||||
|
} else if (CompositorService.isDwl) {
|
||||||
|
isPlaceholder = modelData?.tag === -1;
|
||||||
|
} else if (CompositorService.isSway) {
|
||||||
|
isPlaceholder = modelData?.num === -1;
|
||||||
|
} else {
|
||||||
|
isPlaceholder = modelData === -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlaceholder)
|
||||||
|
return index + 1;
|
||||||
|
|
||||||
|
if (root.useExtWorkspace)
|
||||||
|
return index + 1;
|
||||||
|
if (CompositorService.isHyprland)
|
||||||
|
return modelData?.id || "";
|
||||||
|
if (CompositorService.isDwl)
|
||||||
|
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
|
||||||
|
if (CompositorService.isSway)
|
||||||
|
return modelData?.num || "";
|
||||||
|
return modelData - 1;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway
|
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway
|
||||||
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
|
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
|
||||||
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
||||||
@@ -862,7 +890,18 @@ Item {
|
|||||||
id: rowLayout
|
id: rowLayout
|
||||||
Row {
|
Row {
|
||||||
spacing: 4
|
spacing: 4
|
||||||
visible: loadedIcons.length > 0
|
visible: loadedIcons.length > 0 || SettingsData.showWorkspaceIndex
|
||||||
|
StyledText {
|
||||||
|
topPadding: 2
|
||||||
|
rightPadding: isActive ? 4 : 0
|
||||||
|
visible: SettingsData.showWorkspaceIndex
|
||||||
|
text: {
|
||||||
|
return root.getWorkspaceIndex(modelData);
|
||||||
|
}
|
||||||
|
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
||||||
|
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
||||||
|
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
@@ -1045,31 +1084,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: {
|
text: {
|
||||||
let isPlaceholder;
|
return root.getWorkspaceIndex(modelData);
|
||||||
if (root.useExtWorkspace) {
|
|
||||||
isPlaceholder = modelData?.hidden === true;
|
|
||||||
} else if (CompositorService.isHyprland) {
|
|
||||||
isPlaceholder = modelData?.id === -1;
|
|
||||||
} else if (CompositorService.isDwl) {
|
|
||||||
isPlaceholder = modelData?.tag === -1;
|
|
||||||
} else if (CompositorService.isSway) {
|
|
||||||
isPlaceholder = modelData?.num === -1;
|
|
||||||
} else {
|
|
||||||
isPlaceholder = modelData === -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlaceholder)
|
|
||||||
return index + 1;
|
|
||||||
|
|
||||||
if (root.useExtWorkspace)
|
|
||||||
return index + 1;
|
|
||||||
if (CompositorService.isHyprland)
|
|
||||||
return modelData?.id || "";
|
|
||||||
if (CompositorService.isDwl)
|
|
||||||
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
|
|
||||||
if (CompositorService.isSway)
|
|
||||||
return modelData?.num || "";
|
|
||||||
return modelData - 1;
|
|
||||||
}
|
}
|
||||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
||||||
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
||||||
|
|||||||
@@ -51,17 +51,9 @@ Item {
|
|||||||
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
||||||
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
||||||
|
|
||||||
// Palette that stays stable across track switches until new colors are ready
|
|
||||||
property color dom: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 1.0)
|
|
||||||
property color acc: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.25)
|
|
||||||
property color _nextDom: dom
|
|
||||||
property color _nextAcc: acc
|
|
||||||
|
|
||||||
// Track-switch hold (prevents banner flicker only during switches)
|
|
||||||
property bool isSwitching: false
|
property bool isSwitching: false
|
||||||
property bool paletteReady: false
|
|
||||||
property string _lastArtUrl: ""
|
property string _lastArtUrl: ""
|
||||||
property url _cqSource: ""
|
property string _bgArtSource: ""
|
||||||
|
|
||||||
// Derived "no players" state: always correct, no timers.
|
// Derived "no players" state: always correct, no timers.
|
||||||
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
|
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
|
||||||
@@ -69,7 +61,6 @@ Item {
|
|||||||
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
|
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
|
||||||
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
|
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
|
||||||
|
|
||||||
// Short hold only during track switches (not when players disappear)
|
|
||||||
property bool _switchHold: false
|
property bool _switchHold: false
|
||||||
Timer {
|
Timer {
|
||||||
id: _switchHoldTimer
|
id: _switchHoldTimer
|
||||||
@@ -86,11 +77,9 @@ Item {
|
|||||||
}
|
}
|
||||||
isSwitching = true;
|
isSwitching = true;
|
||||||
_switchHold = true;
|
_switchHold = true;
|
||||||
paletteReady = false;
|
|
||||||
_switchHoldTimer.restart();
|
_switchHoldTimer.restart();
|
||||||
if (activePlayer.trackArtUrl) {
|
if (activePlayer.trackArtUrl)
|
||||||
loadArtwork(activePlayer.trackArtUrl);
|
loadArtwork(activePlayer.trackArtUrl);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property string activeTrackArtFile: ""
|
property string activeTrackArtFile: ""
|
||||||
@@ -108,13 +97,13 @@ Item {
|
|||||||
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
|
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
|
||||||
imageDownloader.targetFile = filename;
|
imageDownloader.targetFile = filename;
|
||||||
imageDownloader.running = true;
|
imageDownloader.running = true;
|
||||||
} else {
|
return;
|
||||||
_preloadImage.source = url;
|
|
||||||
}
|
}
|
||||||
|
_bgArtSource = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeFinishSwitch() {
|
function maybeFinishSwitch() {
|
||||||
if (activePlayer && activePlayer.trackTitle !== "" && paletteReady) {
|
if (activePlayer && activePlayer.trackTitle !== "") {
|
||||||
isSwitching = false;
|
isSwitching = false;
|
||||||
_switchHold = false;
|
_switchHold = false;
|
||||||
}
|
}
|
||||||
@@ -219,9 +208,8 @@ Item {
|
|||||||
property string targetFile: ""
|
property string targetFile: ""
|
||||||
|
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
if (exitCode === 0 && targetFile) {
|
if (exitCode === 0 && targetFile)
|
||||||
_preloadImage.source = "file://" + targetFile;
|
_bgArtSource = "file://" + targetFile;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,121 +218,70 @@ Item {
|
|||||||
running: false
|
running: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
|
||||||
id: _preloadImage
|
|
||||||
source: ""
|
|
||||||
asynchronous: true
|
|
||||||
cache: true
|
|
||||||
visible: false
|
|
||||||
onStatusChanged: {
|
|
||||||
if (status === Image.Ready) {
|
|
||||||
_cqSource = source;
|
|
||||||
colorQuantizer.source = _cqSource;
|
|
||||||
} else if (status === Image.Error) {
|
|
||||||
_cqSource = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorQuantizer {
|
|
||||||
id: colorQuantizer
|
|
||||||
source: _cqSource !== "" ? _cqSource : undefined
|
|
||||||
depth: 8
|
|
||||||
rescaleSize: 32
|
|
||||||
onColorsChanged: {
|
|
||||||
if (!colors || colors.length === 0)
|
|
||||||
return;
|
|
||||||
function enhanceColor(color) {
|
|
||||||
const satBoost = 1.4;
|
|
||||||
const valueBoost = 1.2;
|
|
||||||
return Qt.hsva(color.hsvHue, Math.min(1, color.hsvSaturation * satBoost), Math.min(1, color.hsvValue * valueBoost), color.a);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExtremeColor(startIdx, direction = 1) {
|
|
||||||
let bestColor = colors[startIdx];
|
|
||||||
let bestScore = 0;
|
|
||||||
|
|
||||||
for (let i = startIdx; i >= 0 && i < colors.length; i += direction) {
|
|
||||||
const c = colors[i];
|
|
||||||
const saturation = c.hsvSaturation;
|
|
||||||
const brightness = c.hsvValue;
|
|
||||||
const contrast = Math.abs(brightness - 0.5) * 2;
|
|
||||||
const score = saturation * 0.7 + contrast * 0.3;
|
|
||||||
|
|
||||||
if (score > bestScore) {
|
|
||||||
bestScore = score;
|
|
||||||
bestColor = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return enhanceColor(bestColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingDom = getExtremeColor(Math.floor(colors.length * 0.2), 1);
|
|
||||||
_pendingAcc = getExtremeColor(Math.floor(colors.length * 0.8), -1);
|
|
||||||
paletteApplyDelay.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property color _pendingDom: dom
|
|
||||||
property color _pendingAcc: acc
|
|
||||||
Timer {
|
|
||||||
id: paletteApplyDelay
|
|
||||||
interval: 90
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
const dist = (c1, c2) => {
|
|
||||||
const dr = c1.r - c2.r, dg = c1.g - c2.g, db = c1.b - c2.b;
|
|
||||||
return Math.sqrt(dr * dr + dg * dg + db * db);
|
|
||||||
};
|
|
||||||
const domChanged = dist(_pendingDom, dom) > 0.02;
|
|
||||||
const accChanged = dist(_pendingAcc, acc) > 0.02;
|
|
||||||
if (domChanged || accChanged) {
|
|
||||||
dom = _pendingDom;
|
|
||||||
acc = _pendingAcc;
|
|
||||||
}
|
|
||||||
paletteReady = true;
|
|
||||||
maybeFinishSwitch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property bool isSeeking: false
|
property bool isSeeking: false
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
|
id: bgContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
visible: _bgArtSource !== ""
|
||||||
opacity: 1.0
|
|
||||||
gradient: Gradient {
|
|
||||||
GradientStop {
|
|
||||||
position: 0.0
|
|
||||||
color: Qt.rgba(dom.r, dom.g, dom.b, paletteReady ? 0.38 : 0.06)
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 0.3
|
|
||||||
color: Qt.rgba(acc.r, acc.g, acc.b, paletteReady ? 0.28 : 0.05)
|
|
||||||
}
|
|
||||||
GradientStop {
|
|
||||||
position: 1.0
|
|
||||||
color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, paletteReady ? 0.92 : 0.985)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 160
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on dom {
|
Image {
|
||||||
ColorAnimation {
|
id: bgImage
|
||||||
duration: 220
|
anchors.centerIn: parent
|
||||||
easing.type: Easing.InOutQuad
|
width: Math.max(parent.width, parent.height) * 1.1
|
||||||
|
height: width
|
||||||
|
source: _bgArtSource
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
visible: false
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Ready)
|
||||||
|
maybeFinishSwitch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Behavior on acc {
|
Item {
|
||||||
ColorAnimation {
|
id: blurredBg
|
||||||
duration: 220
|
anchors.fill: parent
|
||||||
easing.type: Easing.InOutQuad
|
visible: false
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: bgImage.width
|
||||||
|
height: bgImage.height
|
||||||
|
source: bgImage
|
||||||
|
blurEnabled: true
|
||||||
|
blurMax: 64
|
||||||
|
blur: 0.8
|
||||||
|
saturation: -0.2
|
||||||
|
brightness: -0.25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bgMask
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
visible: false
|
||||||
|
layer.enabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiEffect {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: blurredBg
|
||||||
|
maskEnabled: true
|
||||||
|
maskSource: bgMask
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1.0
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surface
|
||||||
|
opacity: 0.3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -780,12 +780,18 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
readonly property int remainingSeconds: Math.ceil(SettingsData.powerActionHoldDuration * (1 - root.holdProgress))
|
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
|
||||||
|
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
||||||
text: {
|
text: {
|
||||||
if (root.showHoldHint)
|
if (root.showHoldHint)
|
||||||
return I18n.tr("Hold longer to confirm");
|
return I18n.tr("Hold longer to confirm");
|
||||||
if (root.holdProgress > 0)
|
if (root.holdProgress > 0) {
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(remainingSeconds);
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
|
||||||
|
return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
|
||||||
|
}
|
||||||
|
if (totalMs < 1000)
|
||||||
|
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
|
||||||
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -1086,14 +1086,33 @@ Item {
|
|||||||
Row {
|
Row {
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: NetworkService.networkStatus !== "disconnected" || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio)
|
visible: NetworkService.networkAvailable || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio)
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: NetworkService.networkStatus === "ethernet" ? "lan" : NetworkService.wifiSignalIcon
|
name: {
|
||||||
|
if (NetworkService.wifiToggling)
|
||||||
|
return "sync";
|
||||||
|
switch (NetworkService.networkStatus) {
|
||||||
|
case "ethernet":
|
||||||
|
return "lan";
|
||||||
|
case "vpn":
|
||||||
|
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon;
|
||||||
|
default:
|
||||||
|
return NetworkService.wifiSignalIcon;
|
||||||
|
}
|
||||||
|
}
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5)
|
color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: NetworkService.networkStatus !== "disconnected"
|
visible: NetworkService.networkAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "vpn_lock"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: NetworkService.vpnConnected ? Theme.primary : Qt.rgba(255, 255, 255, 0.5)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
|||||||
@@ -59,12 +59,6 @@ Item {
|
|||||||
"description": I18n.tr("Quick note-taking slideout panel"),
|
"description": I18n.tr("Quick note-taking slideout panel"),
|
||||||
"icon": "sticky_note_2"
|
"icon": "sticky_note_2"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "systemTray",
|
|
||||||
"name": I18n.tr("System Tray"),
|
|
||||||
"description": I18n.tr("System tray icons"),
|
|
||||||
"icon": "notifications"
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +193,8 @@ Item {
|
|||||||
|
|
||||||
DankDropdown {
|
DankDropdown {
|
||||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||||
text: I18n.tr("Night Temperature")
|
text: SessionData.nightModeAutoEnabled ? I18n.tr("Night Temperature") : I18n.tr("Color Temperature")
|
||||||
description: I18n.tr("Color temperature for night mode")
|
description: SessionData.nightModeAutoEnabled ? I18n.tr("Color temperature for night mode") : I18n.tr("Warm color temperature to apply")
|
||||||
currentValue: SessionData.nightModeTemperature + "K"
|
currentValue: SessionData.nightModeTemperature + "K"
|
||||||
options: {
|
options: {
|
||||||
var temps = [];
|
var temps = [];
|
||||||
@@ -223,6 +217,7 @@ Item {
|
|||||||
text: I18n.tr("Day Temperature")
|
text: I18n.tr("Day Temperature")
|
||||||
description: I18n.tr("Color temperature for day time")
|
description: I18n.tr("Color temperature for day time")
|
||||||
currentValue: SessionData.nightModeHighTemperature + "K"
|
currentValue: SessionData.nightModeHighTemperature + "K"
|
||||||
|
visible: SessionData.nightModeAutoEnabled
|
||||||
options: {
|
options: {
|
||||||
var temps = [];
|
var temps = [];
|
||||||
var minTemp = SessionData.nightModeTemperature;
|
var minTemp = SessionData.nightModeTemperature;
|
||||||
@@ -821,7 +816,7 @@ Item {
|
|||||||
const prefs = displaysTab.getScreenPreferences(parent.componentId);
|
const prefs = displaysTab.getScreenPreferences(parent.componentId);
|
||||||
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
|
const isAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
|
||||||
const cid = parent.componentId;
|
const cid = parent.componentId;
|
||||||
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad", "systemTray"].includes(cid) || cid.startsWith("bar:");
|
const isRelevantComponent = ["dankBar", "dock", "notifications", "osd", "toast", "notepad"].includes(cid) || cid.startsWith("bar:");
|
||||||
return !isAll && isRelevantComponent;
|
return !isAll && isRelevantComponent;
|
||||||
}
|
}
|
||||||
onToggled: checked => {
|
onToggled: checked => {
|
||||||
|
|||||||
@@ -597,7 +597,7 @@ Item {
|
|||||||
onSaveBind: (originalKey, newData) => {
|
onSaveBind: (originalKey, newData) => {
|
||||||
KeybindsService.saveBind(originalKey, newData);
|
KeybindsService.saveBind(originalKey, newData);
|
||||||
keybindsTab._editingKey = newData.key;
|
keybindsTab._editingKey = newData.key;
|
||||||
keybindsTab.expandedKey = modelData.action;
|
keybindsTab.expandedKey = newData.action;
|
||||||
}
|
}
|
||||||
onRemoveBind: key => {
|
onRemoveBind: key => {
|
||||||
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
const remainingKey = bindItem.keys.find(k => k.key !== key)?.key ?? "";
|
||||||
|
|||||||
@@ -408,15 +408,27 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("powerActionConfirm", checked)
|
onToggled: checked => SettingsData.set("powerActionConfirm", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsSliderRow {
|
SettingsDropdownRow {
|
||||||
|
id: holdDurationDropdown
|
||||||
|
property var durationOptions: ["250 ms", "500 ms", "750 ms", "1 second", "2 seconds", "3 seconds", "5 seconds", "10 seconds"]
|
||||||
|
property var durationValues: [0.25, 0.5, 0.75, 1, 2, 3, 5, 10]
|
||||||
|
|
||||||
text: I18n.tr("Hold Duration")
|
text: I18n.tr("Hold Duration")
|
||||||
description: I18n.tr("How long to hold the button to confirm the action")
|
options: durationOptions
|
||||||
minimum: 1
|
|
||||||
maximum: 10
|
|
||||||
unit: "s"
|
|
||||||
visible: SettingsData.powerActionConfirm
|
visible: SettingsData.powerActionConfirm
|
||||||
value: SettingsData.powerActionHoldDuration
|
|
||||||
onSliderValueChanged: newValue => SettingsData.set("powerActionHoldDuration", newValue)
|
Component.onCompleted: {
|
||||||
|
const currentDuration = SettingsData.powerActionHoldDuration;
|
||||||
|
const index = durationValues.indexOf(currentDuration);
|
||||||
|
currentValue = index >= 0 ? durationOptions[index] : "500 ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
onValueChanged: value => {
|
||||||
|
const index = durationOptions.indexOf(value);
|
||||||
|
if (index < 0)
|
||||||
|
return;
|
||||||
|
SettingsData.set("powerActionHoldDuration", durationValues[index]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,14 @@ Item {
|
|||||||
"description": I18n.tr("Check for system updates"),
|
"description": I18n.tr("Check for system updates"),
|
||||||
"icon": "update",
|
"icon": "update",
|
||||||
"enabled": SystemUpdateService.distributionSupported
|
"enabled": SystemUpdateService.distributionSupported
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"id": "powerMenuButton",
|
||||||
|
"text": I18n.tr("Power"),
|
||||||
|
"description": I18n.tr("Display the power system menu"),
|
||||||
|
"icon": "power_settings_new",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
var allPluginVariants = PluginService.getAllPluginVariants();
|
var allPluginVariants = PluginService.getAllPluginVariants();
|
||||||
|
|||||||
@@ -506,7 +506,7 @@ Singleton {
|
|||||||
|
|
||||||
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
DMSService.sendRequest("wayland.gamma.setTemperature", {
|
||||||
"low": temperature,
|
"low": temperature,
|
||||||
"high": 6500
|
"high": temperature
|
||||||
}, response => {
|
}, response => {
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error("DisplayService: Failed to set temperature:", response.error);
|
console.error("DisplayService: Failed to set temperature:", response.error);
|
||||||
@@ -752,15 +752,28 @@ Singleton {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: screenChangeRescanTimer
|
id: screenChangeRescanTimer
|
||||||
|
property int rescanAttempt: 0
|
||||||
interval: 3000
|
interval: 3000
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: rescanDevices()
|
onTriggered: {
|
||||||
|
rescanDevices();
|
||||||
|
rescanAttempt++;
|
||||||
|
if (rescanAttempt < 3) {
|
||||||
|
interval = rescanAttempt === 1 ? 5000 : 8000;
|
||||||
|
restart();
|
||||||
|
} else {
|
||||||
|
rescanAttempt = 0;
|
||||||
|
interval = 3000;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Quickshell
|
target: Quickshell
|
||||||
|
|
||||||
function onScreensChanged() {
|
function onScreensChanged() {
|
||||||
|
screenChangeRescanTimer.rescanAttempt = 0;
|
||||||
|
screenChangeRescanTimer.interval = 3000;
|
||||||
screenChangeRescanTimer.restart();
|
screenChangeRescanTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ Item {
|
|||||||
signal popoutClosed
|
signal popoutClosed
|
||||||
signal backgroundClicked
|
signal backgroundClicked
|
||||||
|
|
||||||
|
property var _lastOpenedScreen: null
|
||||||
|
|
||||||
property int effectiveBarPosition: 0
|
property int effectiveBarPosition: 0
|
||||||
property real effectiveBarBottomGap: 0
|
property real effectiveBarBottomGap: 0
|
||||||
|
|
||||||
@@ -100,9 +102,17 @@ Item {
|
|||||||
if (!screen)
|
if (!screen)
|
||||||
return;
|
return;
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
|
|
||||||
|
if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) {
|
||||||
|
contentWindow.visible = false;
|
||||||
|
if (useBackgroundWindow)
|
||||||
|
backgroundWindow.visible = false;
|
||||||
|
}
|
||||||
|
_lastOpenedScreen = screen;
|
||||||
|
|
||||||
shouldBeVisible = true;
|
shouldBeVisible = true;
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
if (shouldBeVisible) {
|
if (shouldBeVisible && screen) {
|
||||||
if (useBackgroundWindow)
|
if (useBackgroundWindow)
|
||||||
backgroundWindow.visible = true;
|
backgroundWindow.visible = true;
|
||||||
contentWindow.visible = true;
|
contentWindow.visible = true;
|
||||||
|
|||||||
@@ -650,9 +650,10 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onWheel: wheel => {
|
onWheel: wheel => {
|
||||||
if (!root.recording)
|
if (!root.recording) {
|
||||||
|
wheel.accepted = false;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
wheel.accepted = true;
|
wheel.accepted = true;
|
||||||
|
|
||||||
const mods = [];
|
const mods = [];
|
||||||
@@ -959,12 +960,12 @@ Item {
|
|||||||
Layout.preferredWidth: 120
|
Layout.preferredWidth: 120
|
||||||
compactMode: true
|
compactMode: true
|
||||||
currentValue: {
|
currentValue: {
|
||||||
const action = root.editAction;
|
const base = root.editAction.split(" ")[0];
|
||||||
const cats = KeybindsService.getCompositorCategories();
|
const cats = KeybindsService.getCompositorCategories();
|
||||||
for (const cat of cats) {
|
for (const cat of cats) {
|
||||||
const actions = KeybindsService.getCompositorActions(cat);
|
const actions = KeybindsService.getCompositorActions(cat);
|
||||||
for (const act of actions) {
|
for (const act of actions) {
|
||||||
if (act.id === action)
|
if (act.id === base)
|
||||||
return cat;
|
return cat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1024,12 +1025,13 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: optionsRow
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(root.editAction)
|
visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(root.editAction)
|
||||||
|
|
||||||
property var argConfig: Actions.getActionArgConfig(root.editAction)
|
readonly property var argConfig: Actions.getActionArgConfig(root.editAction)
|
||||||
property var parsedArgs: Actions.parseCompositorActionArgs(root.editAction)
|
readonly property var parsedArgs: Actions.parseCompositorActionArgs(root.editAction)
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Options")
|
text: I18n.tr("Options")
|
||||||
@@ -1048,56 +1050,75 @@ Item {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 40
|
||||||
visible: {
|
visible: {
|
||||||
const cfg = parent.parent.argConfig;
|
const cfg = optionsRow.argConfig;
|
||||||
if (!cfg || !cfg.config || !cfg.config.args)
|
if (!cfg?.config?.args)
|
||||||
return false;
|
return false;
|
||||||
const firstArg = cfg.config.args[0];
|
const firstArg = cfg.config.args[0];
|
||||||
return firstArg && (firstArg.type === "text" || firstArg.type === "number");
|
return firstArg && (firstArg.type === "text" || firstArg.type === "number");
|
||||||
}
|
}
|
||||||
placeholderText: {
|
placeholderText: optionsRow.argConfig?.config?.args?.[0]?.placeholder || ""
|
||||||
const cfg = parent.parent.argConfig;
|
|
||||||
if (!cfg || !cfg.config || !cfg.config.args)
|
Connections {
|
||||||
return "";
|
target: optionsRow
|
||||||
return cfg.config.args[0]?.placeholder || "";
|
function onParsedArgsChanged() {
|
||||||
|
const newText = optionsRow.parsedArgs?.args?.value || optionsRow.parsedArgs?.args?.index || "";
|
||||||
|
if (argValueField.text !== newText)
|
||||||
|
argValueField.text = newText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
text: parent.parent.parsedArgs?.args?.value || parent.parent.parsedArgs?.args?.index || ""
|
|
||||||
onTextChanged: {
|
Component.onCompleted: {
|
||||||
const cfg = parent.parent.argConfig;
|
text = optionsRow.parsedArgs?.args?.value || optionsRow.parsedArgs?.args?.index || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditingFinished: {
|
||||||
|
const cfg = optionsRow.argConfig;
|
||||||
if (!cfg)
|
if (!cfg)
|
||||||
return;
|
return;
|
||||||
const base = parent.parent.parsedArgs?.base || root.editAction.split(" ")[0];
|
const parsed = optionsRow.parsedArgs;
|
||||||
const args = cfg.config.args[0]?.type === "number" ? {
|
const args = {};
|
||||||
index: text
|
if (cfg.config.args[0]?.type === "number")
|
||||||
} : {
|
args.index = text;
|
||||||
value: text
|
else
|
||||||
};
|
args.value = text;
|
||||||
|
if (parsed?.args?.focus === false)
|
||||||
|
args.focus = false;
|
||||||
root.updateEdit({
|
root.updateEdit({
|
||||||
action: Actions.buildCompositorAction(base, args)
|
action: Actions.buildCompositorAction(parsed?.base || cfg.base, args)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
visible: {
|
visible: {
|
||||||
const cfg = parent.parent.argConfig;
|
const cfg = optionsRow.argConfig;
|
||||||
return cfg && cfg.base === "move-column-to-workspace";
|
if (!cfg)
|
||||||
|
return false;
|
||||||
|
switch (cfg.base) {
|
||||||
|
case "move-column-to-workspace":
|
||||||
|
case "move-column-to-workspace-down":
|
||||||
|
case "move-column-to-workspace-up":
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
DankToggle {
|
DankToggle {
|
||||||
id: focusToggle
|
id: focusToggle
|
||||||
checked: parent.parent.parent.parsedArgs?.args?.focus === true
|
checked: optionsRow.parsedArgs?.args?.focus !== false
|
||||||
onCheckedChanged: {
|
onToggled: newChecked => {
|
||||||
const cfg = parent.parent.parent.argConfig;
|
const cfg = optionsRow.argConfig;
|
||||||
if (!cfg)
|
if (!cfg)
|
||||||
return;
|
return;
|
||||||
const parsed = parent.parent.parent.parsedArgs;
|
const parsed = optionsRow.parsedArgs;
|
||||||
const args = {
|
const args = {};
|
||||||
index: parsed?.args?.index || "",
|
if (cfg.base === "move-column-to-workspace")
|
||||||
focus: checked
|
args.index = parsed?.args?.index || "";
|
||||||
};
|
if (!newChecked)
|
||||||
|
args.focus = false;
|
||||||
root.updateEdit({
|
root.updateEdit({
|
||||||
action: Actions.buildCompositorAction("move-column-to-workspace", args)
|
action: Actions.buildCompositorAction(cfg.base, args)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1110,53 +1131,22 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
visible: {
|
visible: optionsRow.argConfig?.base?.startsWith("screenshot") ?? false
|
||||||
const cfg = parent.parent.argConfig;
|
|
||||||
return cfg && cfg.base && cfg.base.startsWith("screenshot");
|
|
||||||
}
|
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: writeToDiskToggle
|
|
||||||
checked: parent.parent.parent.parent.parsedArgs?.args?.opts?.["write-to-disk"] === "true"
|
|
||||||
onCheckedChanged: {
|
|
||||||
const parsed = parent.parent.parent.parent.parsedArgs;
|
|
||||||
const base = parsed?.base || "screenshot";
|
|
||||||
const opts = parsed?.args?.opts || {};
|
|
||||||
opts["write-to-disk"] = checked ? "true" : "";
|
|
||||||
root.updateEdit({
|
|
||||||
action: Actions.buildCompositorAction(base, {
|
|
||||||
opts: opts
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Save")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
DankToggle {
|
DankToggle {
|
||||||
id: showPointerToggle
|
id: showPointerToggle
|
||||||
checked: parent.parent.parent.parent.parsedArgs?.args?.opts?.["show-pointer"] === "true"
|
checked: optionsRow.parsedArgs?.args?.["show-pointer"] === true
|
||||||
onCheckedChanged: {
|
onToggled: newChecked => {
|
||||||
const parsed = parent.parent.parent.parent.parsedArgs;
|
const parsed = optionsRow.parsedArgs;
|
||||||
const base = parsed?.base || "screenshot";
|
const base = parsed?.base || "screenshot";
|
||||||
const opts = parsed?.args?.opts || {};
|
const args = Object.assign({}, parsed?.args || {});
|
||||||
opts["show-pointer"] = checked ? "true" : "";
|
args["show-pointer"] = newChecked;
|
||||||
root.updateEdit({
|
root.updateEdit({
|
||||||
action: Actions.buildCompositorAction(base, {
|
action: Actions.buildCompositorAction(base, args)
|
||||||
opts: opts
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1167,6 +1157,31 @@ Item {
|
|||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
visible: optionsRow.argConfig?.base !== "screenshot"
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
id: writeToDiskToggle
|
||||||
|
checked: optionsRow.parsedArgs?.args?.["write-to-disk"] === true
|
||||||
|
onToggled: newChecked => {
|
||||||
|
const parsed = optionsRow.parsedArgs;
|
||||||
|
const base = parsed?.base || "screenshot-screen";
|
||||||
|
const args = Object.assign({}, parsed?.args || {});
|
||||||
|
args["write-to-disk"] = newChecked;
|
||||||
|
root.updateEdit({
|
||||||
|
action: Actions.buildCompositorAction(base, args)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,19 @@ Item {
|
|||||||
property color playheadColor: Theme.primary
|
property color playheadColor: Theme.primary
|
||||||
|
|
||||||
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
||||||
function snap(v) { return Math.round(v * dpr) / dpr }
|
function snap(v) {
|
||||||
|
return Math.round(v * dpr) / dpr;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property real playX: snap(root.width * root.value)
|
readonly property real playX: snap(root.width * root.value)
|
||||||
readonly property real midY: snap(height / 2)
|
readonly property real midY: snap(height / 2)
|
||||||
|
|
||||||
Behavior on currentAmp { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } }
|
Behavior on currentAmp {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 300
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
||||||
|
|
||||||
Shape {
|
Shape {
|
||||||
@@ -38,8 +45,16 @@ Item {
|
|||||||
capStyle: ShapePath.RoundCap
|
capStyle: ShapePath.RoundCap
|
||||||
joinStyle: ShapePath.RoundJoin
|
joinStyle: ShapePath.RoundJoin
|
||||||
fillColor: "transparent"
|
fillColor: "transparent"
|
||||||
PathMove { id: flatStart; x: 0; y: root.midY }
|
PathMove {
|
||||||
PathLine { id: flatEnd; x: root.width; y: root.midY }
|
id: flatStart
|
||||||
|
x: Math.min(root.width, snap(root.playX + playhead.width / 2))
|
||||||
|
y: root.midY
|
||||||
|
}
|
||||||
|
PathLine {
|
||||||
|
id: flatEnd
|
||||||
|
x: root.width
|
||||||
|
y: root.midY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +63,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
readonly property real startX: snap(root.lineWidth/2)
|
readonly property real startX: snap(root.lineWidth / 2)
|
||||||
readonly property real aaBias: (0.25 / root.dpr)
|
readonly property real aaBias: (0.25 / root.dpr)
|
||||||
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||||
|
|
||||||
@@ -77,7 +92,10 @@ Item {
|
|||||||
capStyle: ShapePath.RoundCap
|
capStyle: ShapePath.RoundCap
|
||||||
joinStyle: ShapePath.RoundJoin
|
joinStyle: ShapePath.RoundJoin
|
||||||
fillColor: "transparent"
|
fillColor: "transparent"
|
||||||
PathSvg { id: waveSvg; path: "" }
|
PathSvg {
|
||||||
|
id: waveSvg
|
||||||
|
path: ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,8 +106,8 @@ Item {
|
|||||||
height: snap(root.lineWidth)
|
height: snap(root.lineWidth)
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: root.fillColor
|
color: root.fillColor
|
||||||
x: waveClip.startX - width/2
|
x: waveClip.startX - width / 2
|
||||||
y: root.midY - height/2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||||
visible: waveClip.endX > waveClip.startX
|
visible: waveClip.endX > waveClip.startX
|
||||||
z: 2
|
z: 2
|
||||||
}
|
}
|
||||||
@@ -100,8 +118,8 @@ Item {
|
|||||||
height: snap(root.lineWidth)
|
height: snap(root.lineWidth)
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
color: root.fillColor
|
color: root.fillColor
|
||||||
x: waveClip.endX - width/2
|
x: waveClip.endX - width / 2
|
||||||
y: root.midY - height/2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||||
visible: waveClip.endX > waveClip.startX
|
visible: waveClip.endX > waveClip.startX
|
||||||
z: 2
|
z: 2
|
||||||
}
|
}
|
||||||
@@ -119,48 +137,68 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
||||||
function wrapMod(a, m) { let r = a % m; return r < 0 ? r + m : r }
|
function wrapMod(a, m) {
|
||||||
|
let r = a % m;
|
||||||
|
return r < 0 ? r + m : r;
|
||||||
|
}
|
||||||
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
||||||
|
|
||||||
FrameAnimation {
|
FrameAnimation {
|
||||||
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (root.isPlaying) root.phase += 0.03 * frameTime * 60
|
if (root.isPlaying)
|
||||||
startCap.y = root.midY - startCap.height/2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
root.phase += 0.03 * frameTime * 60;
|
||||||
endCap.y = root.midY - endCap.height/2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||||
|
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildStaticWave() {
|
function buildStaticWave() {
|
||||||
const start = waveClip.startX - 2 * root.wavelength
|
const start = waveClip.startX - 2 * root.wavelength;
|
||||||
const end = width + 2 * root.wavelength
|
const end = width + 2 * root.wavelength;
|
||||||
if (end <= start) { waveSvg.path = ""; return }
|
if (end <= start) {
|
||||||
|
waveSvg.path = "";
|
||||||
const kLocal = k
|
return;
|
||||||
const halfPeriod = root.wavelength / 2
|
|
||||||
function y0(x) { return root.midY + root.currentAmp * Math.sin(kLocal * x) }
|
|
||||||
function dy0(x) { return root.currentAmp * Math.cos(kLocal * x) * kLocal }
|
|
||||||
|
|
||||||
let x0 = start
|
|
||||||
let d = `M ${x0} ${y0(x0)}`
|
|
||||||
while (x0 < end) {
|
|
||||||
const x1 = Math.min(x0 + halfPeriod, end)
|
|
||||||
const dx = x1 - x0
|
|
||||||
const yA = y0(x0), yB = y0(x1)
|
|
||||||
const dyA = dy0(x0), dyB = dy0(x1)
|
|
||||||
const c1x = x0 + dx/3
|
|
||||||
const c1y = yA + (dyA * dx)/3
|
|
||||||
const c2x = x1 - dx/3
|
|
||||||
const c2y = yB - (dyB * dx)/3
|
|
||||||
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`
|
|
||||||
x0 = x1
|
|
||||||
}
|
}
|
||||||
waveSvg.path = d
|
|
||||||
|
const kLocal = k;
|
||||||
|
const halfPeriod = root.wavelength / 2;
|
||||||
|
function y0(x) {
|
||||||
|
return root.midY + root.currentAmp * Math.sin(kLocal * x);
|
||||||
|
}
|
||||||
|
function dy0(x) {
|
||||||
|
return root.currentAmp * Math.cos(kLocal * x) * kLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x0 = start;
|
||||||
|
let d = `M ${x0} ${y0(x0)}`;
|
||||||
|
while (x0 < end) {
|
||||||
|
const x1 = Math.min(x0 + halfPeriod, end);
|
||||||
|
const dx = x1 - x0;
|
||||||
|
const yA = y0(x0), yB = y0(x1);
|
||||||
|
const dyA = dy0(x0), dyB = dy0(x1);
|
||||||
|
const c1x = x0 + dx / 3;
|
||||||
|
const c1y = yA + (dyA * dx) / 3;
|
||||||
|
const c2x = x1 - dx / 3;
|
||||||
|
const c2y = yB - (dyB * dx) / 3;
|
||||||
|
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
|
||||||
|
x0 = x1;
|
||||||
|
}
|
||||||
|
waveSvg.path = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: { currentAmp = isPlaying ? amp : 0; buildStaticWave() }
|
Component.onCompleted: {
|
||||||
onWidthChanged: { flatStart.x = 0; flatEnd.x = width; buildStaticWave() }
|
currentAmp = isPlaying ? amp : 0;
|
||||||
|
buildStaticWave();
|
||||||
|
}
|
||||||
|
onWidthChanged: {
|
||||||
|
flatEnd.x = width;
|
||||||
|
buildStaticWave();
|
||||||
|
}
|
||||||
onHeightChanged: buildStaticWave()
|
onHeightChanged: buildStaticWave()
|
||||||
onCurrentAmpChanged: buildStaticWave()
|
onCurrentAmpChanged: buildStaticWave()
|
||||||
onWavelengthChanged: { k = (2 * Math.PI) / Math.max(1e-6, wavelength); buildStaticWave() }
|
onWavelengthChanged: {
|
||||||
|
k = (2 * Math.PI) / Math.max(1e-6, wavelength);
|
||||||
|
buildStaticWave();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -11,21 +11,21 @@ text = '{{colors.background.default.hex}}'
|
|||||||
cursor = '{{colors.primary.default.hex}}'
|
cursor = '{{colors.primary.default.hex}}'
|
||||||
|
|
||||||
[colors.normal]
|
[colors.normal]
|
||||||
black = '{{dank16.color0.hex}}'
|
black = '{{dank16.color0.default.hex}}'
|
||||||
red = '{{dank16.color1.hex}}'
|
red = '{{dank16.color1.default.hex}}'
|
||||||
green = '{{dank16.color2.hex}}'
|
green = '{{dank16.color2.default.hex}}'
|
||||||
yellow = '{{dank16.color3.hex}}'
|
yellow = '{{dank16.color3.default.hex}}'
|
||||||
blue = '{{dank16.color4.hex}}'
|
blue = '{{dank16.color4.default.hex}}'
|
||||||
magenta = '{{dank16.color5.hex}}'
|
magenta = '{{dank16.color5.default.hex}}'
|
||||||
cyan = '{{dank16.color6.hex}}'
|
cyan = '{{dank16.color6.default.hex}}'
|
||||||
white = '{{dank16.color7.hex}}'
|
white = '{{dank16.color7.default.hex}}'
|
||||||
|
|
||||||
[colors.bright]
|
[colors.bright]
|
||||||
black = '{{dank16.color8.hex}}'
|
black = '{{dank16.color8.default.hex}}'
|
||||||
red = '{{dank16.color9.hex}}'
|
red = '{{dank16.color9.default.hex}}'
|
||||||
green = '{{dank16.color10.hex}}'
|
green = '{{dank16.color10.default.hex}}'
|
||||||
yellow = '{{dank16.color11.hex}}'
|
yellow = '{{dank16.color11.default.hex}}'
|
||||||
blue = '{{dank16.color12.hex}}'
|
blue = '{{dank16.color12.default.hex}}'
|
||||||
magenta = '{{dank16.color13.hex}}'
|
magenta = '{{dank16.color13.default.hex}}'
|
||||||
cyan = '{{dank16.color14.hex}}'
|
cyan = '{{dank16.color14.default.hex}}'
|
||||||
white = '{{dank16.color15.hex}}'
|
white = '{{dank16.color15.default.hex}}'
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ background={{colors.background.default.hex_stripped}}
|
|||||||
selection-foreground={{colors.on_surface.default.hex_stripped}}
|
selection-foreground={{colors.on_surface.default.hex_stripped}}
|
||||||
selection-background={{colors.primary_container.default.hex_stripped}}
|
selection-background={{colors.primary_container.default.hex_stripped}}
|
||||||
|
|
||||||
regular0={{dank16.color0.hex_stripped}}
|
regular0={{dank16.color0.default.hex_stripped}}
|
||||||
regular1={{dank16.color1.hex_stripped}}
|
regular1={{dank16.color1.default.hex_stripped}}
|
||||||
regular2={{dank16.color2.hex_stripped}}
|
regular2={{dank16.color2.default.hex_stripped}}
|
||||||
regular3={{dank16.color3.hex_stripped}}
|
regular3={{dank16.color3.default.hex_stripped}}
|
||||||
regular4={{dank16.color4.hex_stripped}}
|
regular4={{dank16.color4.default.hex_stripped}}
|
||||||
regular5={{dank16.color5.hex_stripped}}
|
regular5={{dank16.color5.default.hex_stripped}}
|
||||||
regular6={{dank16.color6.hex_stripped}}
|
regular6={{dank16.color6.default.hex_stripped}}
|
||||||
regular7={{dank16.color7.hex_stripped}}
|
regular7={{dank16.color7.default.hex_stripped}}
|
||||||
bright0={{dank16.color8.hex_stripped}}
|
bright0={{dank16.color8.default.hex_stripped}}
|
||||||
bright1={{dank16.color9.hex_stripped}}
|
bright1={{dank16.color9.default.hex_stripped}}
|
||||||
bright2={{dank16.color10.hex_stripped}}
|
bright2={{dank16.color10.default.hex_stripped}}
|
||||||
bright3={{dank16.color11.hex_stripped}}
|
bright3={{dank16.color11.default.hex_stripped}}
|
||||||
bright4={{dank16.color12.hex_stripped}}
|
bright4={{dank16.color12.default.hex_stripped}}
|
||||||
bright5={{dank16.color13.hex_stripped}}
|
bright5={{dank16.color13.default.hex_stripped}}
|
||||||
bright6={{dank16.color14.hex_stripped}}
|
bright6={{dank16.color14.default.hex_stripped}}
|
||||||
bright7={{dank16.color15.hex_stripped}}
|
bright7={{dank16.color15.default.hex_stripped}}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user