1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 07:52:50 -05:00

Compare commits

...

86 Commits

Author SHA1 Message Date
Marcus Ramberg
8fad2826b1 ci: add flake check 2025-12-08 16:09:16 +01:00
bbedward
57ee0fb2bd bump: failed fprint tries 2025-12-08 10:02:53 -05:00
osscar
3ef10e73a5 nix: remove leading dot in nativeBuildInputs (#948)
Co-authored-by: osscar <osscar.unheard025@passmail.net>
2025-12-08 15:52:32 +01:00
bbedward
dc40492fc7 cc: fix audio slider binding 2025-12-08 09:45:25 -05:00
bbedward
e606a76a86 screenshot: add screenshot-window support for DWL/MangoWC 2025-12-08 09:39:42 -05:00
Lucas
8838fd67b9 nix: add dev-shell (#944)
* nix: add dev-shell

* docs: add Nix dev shell in contributing docs
2025-12-08 12:22:07 +01:00
Lucas
c570e20308 nix: use quickshell from source by default in greeter (#941) 2025-12-08 07:37:29 +01:00
bbedward
0a00ef39e3 ipc: fix bar widget IPCs when screens change 2025-12-07 23:15:24 -05:00
bbedward
9a08b81214 dankinstall: swap to systemd by default, use 90-dms.conf for vars 2025-12-07 22:51:22 -05:00
bbedward
c617ae26a2 niri: fix some keybind tab issues
- Fix args for screenshot
- move-column stuff is focus=true by default
- Parsing fixes
part of #914
2025-12-07 22:41:01 -05:00
Lucas
f6a776a692 nix: use by default quickshell from source (#939) 2025-12-07 21:11:22 -05:00
bbedward
54b253099d dankinstall: update hyprland syntax
fixes #913
2025-12-07 21:03:24 -05:00
bbedward
f662aca58c dankinstall: replace grim+slurp+grimblast with dms 2025-12-07 20:59:46 -05:00
bbedward
76e7755496 consistent icon sizing 2025-12-07 20:21:07 -05:00
bbedward
e05ad81c13 displays: remove system tray per-display opt
- superceded by omegabar
2025-12-07 20:13:40 -05:00
bbedward
cffb16d7f7 matugen: make signalByName helper not use exec 2025-12-07 20:10:31 -05:00
bbedward
18ca571944 matugen: scrap shell script for proper backend implementation with queue
system
2025-12-07 20:00:43 -05:00
bbedward
3ae1973e21 screenshot/colorpicker: fix scaling, update go-wayland to fix object
destruction, fix hyprland window detection
2025-12-07 13:44:35 -05:00
bbedward
308c8c3ea7 lock screen: fix inconsistency with network status, add VPN
maybe fix #926
2025-12-07 12:33:29 -05:00
bbedward
f49b5dd037 media player: replace color quantizer with album art 2025-12-07 12:23:00 -05:00
bbedward
f245ba82ad gamma: fix non-automation toggling
fixes #924
2025-12-07 12:02:50 -05:00
arfan
60d22d6973 feat: add workspace index display when app icon enabled (#936) 2025-12-07 11:48:48 -05:00
Farokh
d6f48a82d9 Update VSCode color theme templates for improved contrast and readability (#931)
* matugen/vscode-theme: update VSCode templates for contrast and readability

* vscode-theme: rework dark theme, refine light, restore default fallback

* dank16: add variants option, make default vscode consistent, fix termial
always dark

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-12-07 11:47:25 -05:00
Marcus Ramberg
c0d73dae67 fix: handle ipc arguments (#930) 2025-12-07 11:01:31 -05:00
Marcus Ramberg
49eb60589d fix: also restart ghostty/kitty on nix (#934) 2025-12-07 10:28:26 -05:00
Marcus Ramberg
89993b7421 core: remove unused function after refactors (#935) 2025-12-07 10:27:44 -05:00
purian23
511cb93806 Update rebuild logic on automation to obs / ppa 2025-12-06 21:33:53 -05:00
purian23
8ce78e7134 Dependency removals from Dankinstaller Distros
- Removed grim, grimblast, slurp, hyprpicker & mate-polkit from all distros
2025-12-06 01:10:13 -05:00
Yuxiang Qiu
9ebfab2e78 brightness: rescan brightness (#922) 2025-12-06 00:24:54 -05:00
bbedward
833d245251 dankbar: fix centersection positioning 2025-12-05 23:59:06 -05:00
bbedward
00d3024143 dankbar: keep border on maximize 2025-12-05 23:50:27 -05:00
bbedward
aedeab8a6a screenshot: add window capture for Hyprland 2025-12-05 21:10:12 -05:00
Pi Home Server
4d39169eb8 Feature/control center widget fix (#912)
* Add a widget to display the power menu

* Update power button widget

* Upate based on new settings

* Rollback to DisplaysTab.qml
2025-12-05 20:29:39 -05:00
bbedward
2ddc448150 screenshot: ensure screencopy before surface creation 2025-12-05 17:39:35 -05:00
bbedward
f9a6b4ce2c colorpick/screenshot: make color-format aware 2025-12-05 17:26:38 -05:00
bbedward
22b2b69413 screenshot: add shift to perfect-square capability 2025-12-05 17:08:00 -05:00
bbedward
7f11632ea6 screenshot: fix notif content to show open file browser 2025-12-05 16:56:29 -05:00
bbedward
c0b4d5e2c2 screenshot: fix thumbnail preview 2025-12-05 16:16:13 -05:00
Lucas
2c23d0249c nix: match upstream package format (#918) 2025-12-05 16:11:18 -05:00
bbedward
c3233fbf61 power menu: shorter hold durations 2025-12-05 16:05:11 -05:00
bbedward
ecfc8e208c screenshot: clipboard by default 2025-12-05 15:59:37 -05:00
bbedward
52d5e21fc4 screenshot: fix some region mappings 2025-12-05 15:25:27 -05:00
bbedward
6d0c56554f core: add screenshot utility 2025-12-05 14:59:34 -05:00
bbedward
844e91dc9e controlcenter: default vpn button to on 2025-12-05 14:21:19 -05:00
bbedward
1f00b5f577 fix some stale screen ref issues in OSD and popout 2025-12-05 13:31:57 -05:00
bbedward
2c48458384 brightness: more aggressive ddc rescans on device changes 2025-12-05 13:18:10 -05:00
bbedward
ddda87c5a7 less agress dms-open MimeType declarations 2025-12-05 12:36:04 -05:00
bbedward
6b1bbca620 keybinds: fix alt+shift, kdl parsing, allow arguments 2025-12-05 12:31:15 -05:00
bbedward
b5378e5d3c hypr: add exclusive focus override 2025-12-05 10:37:24 -05:00
bbedward
c69a55df29 flickable: update momentum scrolling logic 2025-12-05 10:14:16 -05:00
bbedward
5faa1a993a launcher: reemove background from list and add a bottom fade 2025-12-05 10:04:19 -05:00
bbedward
e56481f6d7 launcher: add 1px gap between grid delegates 2025-12-05 09:33:04 -05:00
bbedward
f9610d457c dankbar: fix border thickness 2025-12-05 09:29:45 -05:00
bbedward
ae066f42a4 brightness: delay screen change rescan of devices 2025-12-04 23:10:25 -05:00
bbedward
c60dd42fa7 dankinstall: set default niri config with includes 2025-12-04 22:45:46 -05:00
Yuxiang Qiu
7aac5ac5a1 dankbar: fix privacy indicator background color (#909) 2025-12-04 21:32:48 -05:00
bbedward
ad0f3fa33b dankbar: convert center section to use WidgetHost 2025-12-04 19:37:21 -05:00
bbedward
63d121b796 proc: ability to run command with noTimeout 2025-12-04 16:09:38 -05:00
bbedward
4291cfe82f settings: fix launcher tab sizing 2025-12-04 16:01:07 -05:00
bbedward
f312868154 lock: respect confirmation mode power actions 2025-12-04 14:58:36 -05:00
bbedward
5b42d34ac8 expose iconSize helpers to plugins 2025-12-04 14:40:38 -05:00
bbedward
397a8c275d settings: add IPCs to open specific settings tabs 2025-12-04 14:31:35 -05:00
purian23
2aabee453b Remove hyprpicker requirement for DMS Copr 2025-12-04 14:25:40 -05:00
bbedward
185333a615 brightness: default IPCs to pinned devices per-display
fixes #875
2025-12-04 13:55:14 -05:00
bbedward
7d177eb1d4 greeter: fix mango config override
fixes #904
2025-12-04 13:46:11 -05:00
Givani Boekestijn
705a84051d feat(dankdash): add vim keybindings (hjkl) to wallpaper picker navigation (#903) 2025-12-04 13:22:30 -05:00
bbedward
f6821f80e1 dankslideout: convert to Rectangle 2025-12-04 12:54:15 -05:00
bbedward
e7a6f5228d widgets: fix binding loop in button 2025-12-04 12:50:06 -05:00
bbedward
8161fd6acb i18n: add hebrew *partial*
- Most widgets and components lack proper RTL layout support
- Merging hebrew anyway, as these can be updated incrementally later
2025-12-04 11:39:58 -05:00
bbedward
2137920e81 dankslideout: put opacity on parent layer 2025-12-04 10:06:45 -05:00
bbedward
879102599c matugen: package vscode theme as vsix 2025-12-04 09:39:29 -05:00
bbedward
44190f07fe colorpicker: hide magnifier on startup 2025-12-04 09:14:12 -05:00
bbedward
a41487eb8f colorpicker: hide magnfier on monitor leave 2025-12-04 09:12:21 -05:00
bbedward
e1acaaa27c dankbar: add option to disable maximize detection
fixes #895
2025-12-04 08:56:04 -05:00
Marcus Ramberg
08a97aeff8 power: support automatic profile switching on battery change (#897) 2025-12-04 08:37:07 -05:00
bbedward
5b7302b46d color picker: use shortcuts inhibitor when active 2025-12-04 00:08:43 -05:00
purian23
34c0bba130 Add Debian / Ubuntu / OpenSuse support to DankInstaller 2025-12-03 23:41:17 -05:00
bbedward
5a53447272 color picker: switch to dms picker 2025-12-03 23:18:46 -05:00
bbedward
b6847289ff keybinds tab: change colors 2025-12-03 23:14:20 -05:00
bbedward
d22c43e08b app picker: fix background close 2025-12-03 23:00:22 -05:00
bbedward
d9deaa8d74 cli: add interactive color picker 2025-12-03 22:29:57 -05:00
bbedward
6c7776a9a6 audio: add IPC & OSD for changing output audio device
fixes #754
2025-12-03 20:47:57 -05:00
bbedward
62bd6e41ef settings: break out dank bar widgets 2025-12-03 18:17:06 -05:00
bbedward
293c7b42c6 pass screen to modals 2025-12-03 17:27:07 -05:00
bbedward
788da62777 settings: mecha re-organization 2025-12-03 17:25:40 -05:00
bbedward
2c7f24a913 lock: add option to show on 1 display
fixes #607
2025-12-03 12:15:22 -05:00
224 changed files with 31995 additions and 17295 deletions

View File

@@ -37,7 +37,10 @@ if [[ -n "$STAGED_CORE_FILES" ]]; then
# Tests # Tests
echo " Running tests..." echo " Running tests..."
go test ./... >/dev/null if ! go test ./... >/dev/null 2>&1; then
echo "Tests failed! Run 'go test ./...' for details."
exit 1
fi
# Build checks # Build checks
echo " Building..." echo " Building..."

30
.github/workflows/nix-pr-check.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Check nix flake
on:
pull_request:
branches: [master, main]
paths:
- "flake.*"
- "distro/nix/**"
jobs:
check-flake:
runs-on: ubuntu-latest
steps:
- name: Create GitHub App token
id: app_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app_token.outputs.token }}
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Update vendorHash in flake.nix
run: nix flake check

View File

@@ -517,7 +517,6 @@ jobs:
Recommends: cava Recommends: cava
Recommends: cliphist Recommends: cliphist
Recommends: danksearch Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen Recommends: matugen
Recommends: wl-clipboard Recommends: wl-clipboard
Recommends: NetworkManager Recommends: NetworkManager

37
.gitignore vendored
View File

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

View File

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

View File

@@ -6,5 +6,5 @@ Exec=dms open %u
Icon=danklogo Icon=danklogo
Terminal=false Terminal=false
NoDisplay=true NoDisplay=true
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/file;text/html; MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
Categories=Utility; Categories=Utility;

View File

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

View File

@@ -16,6 +16,9 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
**Wayland Protocols** **Wayland Protocols**
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control - `wlr-gamma-control-unstable-v1` - Night mode and gamma control
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
- `wp-viewporter` - Fractional scaling support
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration - `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
- `ext-workspace-v1` - Workspace protocol support - `ext-workspace-v1` - Workspace protocol support
- `wlr-output-management-unstable-v1` - Display configuration - `wlr-output-management-unstable-v1` - Display configuration
@@ -44,9 +47,24 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.) - `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
- `dms plugins [install|browse|search]` - Plugin management - `dms plugins [install|browse|search]` - Plugin management
- `dms brightness [list|set]` - Control display/monitor brightness - `dms brightness [list|set]` - Control display/monitor brightness
- `dms color pick` - Native color picker (see below)
- `dms update` - Update DMS and dependencies (disabled in distro packages) - `dms update` - Update DMS and dependencies (disabled in distro packages)
- `dms greeter install` - Install greetd greeter (disabled in distro packages) - `dms greeter install` - Install greetd greeter (disabled in distro packages)
### Color Picker
Native Wayland color picker with magnifier, no external dependencies. Supports HiDPI and fractional scaling.
```bash
dms color pick # Pick color, output hex
dms color pick --rgb # Output as RGB (255 128 64)
dms color pick --hsv # Output as HSV (24 75% 100%)
dms color pick --json # Output all formats as JSON
dms color pick -a # Auto-copy to clipboard
```
The on-screen preview displays the selected format. JSON output includes hex, RGB, HSL, HSV, and CMYK values.
## Building ## Building
Requires Go 1.24+ Requires Go 1.24+

View File

@@ -0,0 +1,133 @@
package main
import (
"fmt"
"os"
"os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
"github.com/spf13/cobra"
)
var (
colorOutputFmt string
colorAutocopy bool
colorNotify bool
colorLowercase bool
)
var colorCmd = &cobra.Command{
Use: "color",
Short: "Color utilities",
Long: "Color utilities including picking colors from the screen",
}
var colorPickCmd = &cobra.Command{
Use: "pick",
Short: "Pick a color from the screen",
Long: `Pick a color from anywhere on your screen using an interactive color picker.
Click on any pixel to capture its color, or press Escape to cancel.
Output format flags (mutually exclusive, default: --hex):
--hex - Hexadecimal (#RRGGBB)
--rgb - RGB values (R G B)
--hsl - HSL values (H S% L%)
--hsv - HSV values (H S% V%)
--cmyk - CMYK values (C% M% Y% K%)
--json - JSON with all formats
Examples:
dms color pick # Pick color, output as hex
dms color pick --rgb # Output as RGB
dms color pick --json # Output all formats as JSON
dms color pick --hex -l # Output hex in lowercase
dms color pick -a # Auto-copy result to clipboard`,
Run: runColorPick,
}
func init() {
colorPickCmd.Flags().Bool("hex", false, "Output as hexadecimal (#RRGGBB)")
colorPickCmd.Flags().Bool("rgb", false, "Output as RGB (R G B)")
colorPickCmd.Flags().Bool("hsl", false, "Output as HSL (H S% L%)")
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
colorCmd.AddCommand(colorPickCmd)
}
func runColorPick(cmd *cobra.Command, args []string) {
format := colorpicker.FormatHex // default
jsonOutput, _ := cmd.Flags().GetBool("json")
if rgb, _ := cmd.Flags().GetBool("rgb"); rgb {
format = colorpicker.FormatRGB
} else if hsl, _ := cmd.Flags().GetBool("hsl"); hsl {
format = colorpicker.FormatHSL
} else if hsv, _ := cmd.Flags().GetBool("hsv"); hsv {
format = colorpicker.FormatHSV
} else if cmyk, _ := cmd.Flags().GetBool("cmyk"); cmyk {
format = colorpicker.FormatCMYK
}
config := colorpicker.Config{
Format: format,
CustomFormat: colorOutputFmt,
Lowercase: colorLowercase,
Autocopy: colorAutocopy,
Notify: colorNotify,
}
picker := colorpicker.New(config)
color, err := picker.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if color == nil {
os.Exit(0)
}
var output string
if jsonOutput {
jsonStr, err := color.ToJSON()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
output = jsonStr
} else {
output = color.Format(config.Format, config.Lowercase, config.CustomFormat)
}
if colorAutocopy {
copyToClipboard(output)
}
if jsonOutput {
fmt.Println(output)
} else if color.IsDark() {
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
} else {
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
}
}
func copyToClipboard(text string) {
var cmd *exec.Cmd
if _, err := exec.LookPath("wl-copy"); err == nil {
cmd = exec.Command("wl-copy", text)
} else {
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
return
}
_ = cmd.Run()
}

View File

@@ -454,7 +454,6 @@ func uninstallPluginCLI(idOrName string) error {
return nil return nil
} }
// getCommonCommands returns the commands available in all builds
func getCommonCommands() []*cobra.Command { func getCommonCommands() []*cobra.Command {
return []*cobra.Command{ return []*cobra.Command{
versionCmd, versionCmd,
@@ -471,5 +470,9 @@ func getCommonCommands() []*cobra.Command {
keybindsCmd, keybindsCmd,
greeterCmd, greeterCmd,
setupCmd, setupCmd,
colorCmd,
screenshotCmd,
notifyActionCmd,
matugenCmd,
} }
} }

View File

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

View File

@@ -0,0 +1,182 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
"github.com/spf13/cobra"
)
var matugenCmd = &cobra.Command{
Use: "matugen",
Short: "Generate Material Design themes",
Long: "Generate Material Design themes using matugen with dank16 color integration",
}
var matugenGenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate theme synchronously",
Run: runMatugenGenerate,
}
var matugenQueueCmd = &cobra.Command{
Use: "queue",
Short: "Queue theme generation (uses socket if available)",
Run: runMatugenQueue,
}
func init() {
matugenCmd.AddCommand(matugenGenerateCmd)
matugenCmd.AddCommand(matugenQueueCmd)
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
cmd.Flags().String("state-dir", "", "State directory for cache files")
cmd.Flags().String("shell-dir", "", "DMS shell installation directory")
cmd.Flags().String("config-dir", "", "User config directory")
cmd.Flags().String("kind", "image", "Source type: image or hex")
cmd.Flags().String("value", "", "Wallpaper path or hex color")
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
}
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
}
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
stateDir, _ := cmd.Flags().GetString("state-dir")
shellDir, _ := cmd.Flags().GetString("shell-dir")
configDir, _ := cmd.Flags().GetString("config-dir")
kind, _ := cmd.Flags().GetString("kind")
value, _ := cmd.Flags().GetString("value")
mode, _ := cmd.Flags().GetString("mode")
iconTheme, _ := cmd.Flags().GetString("icon-theme")
matugenType, _ := cmd.Flags().GetString("matugen-type")
runUserTemplates, _ := cmd.Flags().GetBool("run-user-templates")
stockColors, _ := cmd.Flags().GetString("stock-colors")
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
return matugen.Options{
StateDir: stateDir,
ShellDir: shellDir,
ConfigDir: configDir,
Kind: kind,
Value: value,
Mode: mode,
IconTheme: iconTheme,
MatugenType: matugenType,
RunUserTemplates: runUserTemplates,
StockColors: stockColors,
SyncModeWithPortal: syncModeWithPortal,
TerminalsAlwaysDark: terminalsAlwaysDark,
}
}
func runMatugenGenerate(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
}
func runMatugenQueue(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd)
wait, _ := cmd.Flags().GetBool("wait")
timeout, _ := cmd.Flags().GetDuration("timeout")
socketPath := os.Getenv("DMS_SOCKET")
if socketPath == "" {
var err error
socketPath, err = server.FindSocket()
if err != nil {
log.Info("No socket available, running synchronously")
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
return
}
}
conn, err := net.Dial("unix", socketPath)
if err != nil {
log.Info("Socket connection failed, running synchronously")
if err := matugen.Run(opts); err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
return
}
defer conn.Close()
request := map[string]any{
"id": 1,
"method": "matugen.queue",
"params": map[string]any{
"stateDir": opts.StateDir,
"shellDir": opts.ShellDir,
"configDir": opts.ConfigDir,
"kind": opts.Kind,
"value": opts.Value,
"mode": opts.Mode,
"iconTheme": opts.IconTheme,
"matugenType": opts.MatugenType,
"runUserTemplates": opts.RunUserTemplates,
"stockColors": opts.StockColors,
"syncModeWithPortal": opts.SyncModeWithPortal,
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
"wait": wait,
},
}
if err := json.NewEncoder(conn).Encode(request); err != nil {
log.Fatalf("Failed to send request: %v", err)
}
if !wait {
fmt.Println("Theme generation queued")
return
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resultCh := make(chan error, 1)
go func() {
var response struct {
ID int `json:"id"`
Result any `json:"result"`
Error string `json:"error"`
}
if err := json.NewDecoder(conn).Decode(&response); err != nil {
resultCh <- fmt.Errorf("failed to read response: %w", err)
return
}
if response.Error != "" {
resultCh <- fmt.Errorf("server error: %s", response.Error)
return
}
resultCh <- nil
}()
select {
case err := <-resultCh:
if err != nil {
log.Fatalf("Theme generation failed: %v", err)
}
fmt.Println("Theme generation completed")
case <-ctx.Done():
log.Fatalf("Timeout waiting for theme generation")
}
}

View File

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

View File

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

View File

@@ -0,0 +1,306 @@
package colorpicker
import (
"encoding/json"
"fmt"
"math"
"strings"
)
type Color struct {
R, G, B, A uint8
}
type OutputFormat int
const (
FormatHex OutputFormat = iota
FormatRGB
FormatHSL
FormatHSV
FormatCMYK
)
func ParseFormat(s string) OutputFormat {
switch strings.ToLower(s) {
case "rgb":
return FormatRGB
case "hsl":
return FormatHSL
case "hsv":
return FormatHSV
case "cmyk":
return FormatCMYK
default:
return FormatHex
}
}
func (c Color) ToHex(lowercase bool) string {
if lowercase {
return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}
func (c Color) ToRGB() string {
return fmt.Sprintf("%d %d %d", c.R, c.G, c.B)
}
func (c Color) ToHSL() string {
h, s, l := rgbToHSL(c.R, c.G, c.B)
return fmt.Sprintf("%d %d%% %d%%", h, s, l)
}
func (c Color) ToHSV() string {
h, s, v := rgbToHSV(c.R, c.G, c.B)
return fmt.Sprintf("%d %d%% %d%%", h, s, v)
}
func (c Color) ToCMYK() string {
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
return fmt.Sprintf("%d%% %d%% %d%% %d%%", cy, m, y, k)
}
func (c Color) Format(format OutputFormat, lowercase bool, customFmt string) string {
if customFmt != "" {
return c.formatCustom(format, customFmt)
}
switch format {
case FormatRGB:
return c.ToRGB()
case FormatHSL:
return c.ToHSL()
case FormatHSV:
return c.ToHSV()
case FormatCMYK:
return c.ToCMYK()
default:
return c.ToHex(lowercase)
}
}
func (c Color) formatCustom(format OutputFormat, customFmt string) string {
switch format {
case FormatRGB:
return replaceArgs(customFmt, c.R, c.G, c.B)
case FormatHSL:
h, s, l := rgbToHSL(c.R, c.G, c.B)
return replaceArgs(customFmt, h, s, l)
case FormatHSV:
h, s, v := rgbToHSV(c.R, c.G, c.B)
return replaceArgs(customFmt, h, s, v)
case FormatCMYK:
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
return replaceArgs4(customFmt, cy, m, y, k)
default:
if strings.Contains(customFmt, "{0}") {
r := fmt.Sprintf("%02X", c.R)
g := fmt.Sprintf("%02X", c.G)
b := fmt.Sprintf("%02X", c.B)
return replaceArgsStr(customFmt, r, g, b)
}
return c.ToHex(false)
}
}
func replaceArgs[T any](format string, a, b, c T) string {
result := format
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
return result
}
func replaceArgs4[T any](format string, a, b, c, d T) string {
result := format
result = strings.ReplaceAll(result, "{0}", fmt.Sprintf("%v", a))
result = strings.ReplaceAll(result, "{1}", fmt.Sprintf("%v", b))
result = strings.ReplaceAll(result, "{2}", fmt.Sprintf("%v", c))
result = strings.ReplaceAll(result, "{3}", fmt.Sprintf("%v", d))
return result
}
func replaceArgsStr(format, a, b, c string) string {
result := format
result = strings.ReplaceAll(result, "{0}", a)
result = strings.ReplaceAll(result, "{1}", b)
result = strings.ReplaceAll(result, "{2}", c)
return result
}
func rgbToHSL(r, g, b uint8) (int, int, int) {
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
maxVal := math.Max(rf, math.Max(gf, bf))
minVal := math.Min(rf, math.Min(gf, bf))
l := (maxVal + minVal) / 2
if maxVal == minVal {
return 0, 0, int(math.Round(l * 100))
}
d := maxVal - minVal
var s float64
if l > 0.5 {
s = d / (2 - maxVal - minVal)
} else {
s = d / (maxVal + minVal)
}
var h float64
switch maxVal {
case rf:
h = (gf - bf) / d
if gf < bf {
h += 6
}
case gf:
h = (bf-rf)/d + 2
case bf:
h = (rf-gf)/d + 4
}
h /= 6
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(l * 100))
}
func rgbToHSV(r, g, b uint8) (int, int, int) {
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
maxVal := math.Max(rf, math.Max(gf, bf))
minVal := math.Min(rf, math.Min(gf, bf))
v := maxVal
d := maxVal - minVal
var s float64
if maxVal != 0 {
s = d / maxVal
}
if maxVal == minVal {
return 0, int(math.Round(s * 100)), int(math.Round(v * 100))
}
var h float64
switch maxVal {
case rf:
h = (gf - bf) / d
if gf < bf {
h += 6
}
case gf:
h = (bf-rf)/d + 2
case bf:
h = (rf-gf)/d + 4
}
h /= 6
return int(math.Round(h * 360)), int(math.Round(s * 100)), int(math.Round(v * 100))
}
func rgbToCMYK(r, g, b uint8) (int, int, int, int) {
if r == 0 && g == 0 && b == 0 {
return 0, 0, 0, 100
}
rf := float64(r) / 255.0
gf := float64(g) / 255.0
bf := float64(b) / 255.0
k := 1 - math.Max(rf, math.Max(gf, bf))
c := (1 - rf - k) / (1 - k)
m := (1 - gf - k) / (1 - k)
y := (1 - bf - k) / (1 - k)
return int(math.Round(c * 100)), int(math.Round(m * 100)), int(math.Round(y * 100)), int(math.Round(k * 100))
}
func (c Color) Luminance() float64 {
r := float64(c.R) / 255.0
g := float64(c.G) / 255.0
b := float64(c.B) / 255.0
if r <= 0.03928 {
r = r / 12.92
} else {
r = math.Pow((r+0.055)/1.055, 2.4)
}
if g <= 0.03928 {
g = g / 12.92
} else {
g = math.Pow((g+0.055)/1.055, 2.4)
}
if b <= 0.03928 {
b = b / 12.92
} else {
b = math.Pow((b+0.055)/1.055, 2.4)
}
return 0.2126*r + 0.7152*g + 0.0722*b
}
func (c Color) IsDark() bool {
return c.Luminance() < 0.179
}
type ColorJSON struct {
Hex string `json:"hex"`
RGB struct {
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
} `json:"rgb"`
HSL struct {
H int `json:"h"`
S int `json:"s"`
L int `json:"l"`
} `json:"hsl"`
HSV struct {
H int `json:"h"`
S int `json:"s"`
V int `json:"v"`
} `json:"hsv"`
CMYK struct {
C int `json:"c"`
M int `json:"m"`
Y int `json:"y"`
K int `json:"k"`
} `json:"cmyk"`
}
func (c Color) ToJSON() (string, error) {
h, s, l := rgbToHSL(c.R, c.G, c.B)
hv, sv, v := rgbToHSV(c.R, c.G, c.B)
cy, m, y, k := rgbToCMYK(c.R, c.G, c.B)
data := ColorJSON{
Hex: c.ToHex(false),
}
data.RGB.R = int(c.R)
data.RGB.G = int(c.G)
data.RGB.B = int(c.B)
data.HSL.H = h
data.HSL.S = s
data.HSL.L = l
data.HSV.H = hv
data.HSV.S = sv
data.HSV.V = v
data.CMYK.C = cy
data.CMYK.M = m
data.CMYK.Y = y
data.CMYK.K = k
bytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return "", err
}
return string(bytes), nil
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -110,7 +110,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return results, nil return results, nil
} }
// deployNiriConfig handles Niri configuration deployment with backup and merging
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) { func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
result := DeploymentResult{ result := DeploymentResult{
ConfigType: "Niri", ConfigType: "Niri",
@@ -123,6 +122,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
return result, result.Error return result, result.Error
} }
dmsDir := filepath.Join(configDir, "dms")
if err := os.MkdirAll(dmsDir, 0755); err != nil {
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
return result, result.Error
}
var existingConfig string var existingConfig string
if _, err := os.Stat(result.Path); err == nil { if _, err := os.Stat(result.Path); err == nil {
cd.log("Found existing Niri configuration") cd.log("Found existing Niri configuration")
@@ -143,14 +148,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
} }
// Detect polkit agent path
polkitPath, err := cd.detectPolkitAgent() polkitPath, err := cd.detectPolkitAgent()
if err != nil { if err != nil {
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err)) cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
} }
// Determine terminal command based on choice
var terminalCommand string var terminalCommand string
switch terminal { switch terminal {
case deps.TerminalGhostty: case deps.TerminalGhostty:
@@ -160,13 +163,12 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
case deps.TerminalAlacritty: case deps.TerminalAlacritty:
terminalCommand = "alacritty" terminalCommand = "alacritty"
default: default:
terminalCommand = "ghostty" // fallback to ghostty terminalCommand = "ghostty"
} }
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath) newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand) newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
// If there was an existing config, merge the output sections
if existingConfig != "" { if existingConfig != "" {
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig) mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
if err != nil { if err != nil {
@@ -182,11 +184,38 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
return result, result.Error return result, result.Error
} }
if err := cd.deployNiriDmsConfigs(dmsDir, terminalCommand); err != nil {
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
return result, result.Error
}
result.Deployed = true result.Deployed = true
cd.log("Successfully deployed Niri configuration") cd.log("Successfully deployed Niri configuration")
return result, nil return result, nil
} }
func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) error {
configs := []struct {
name string
content string
}{
{"colors.kdl", NiriColorsConfig},
{"layout.kdl", NiriLayoutConfig},
{"alttab.kdl", NiriAlttabConfig},
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
}
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
}
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
}
return nil
}
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) { func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult

View File

@@ -435,7 +435,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
content, err := os.ReadFile(result.Path) content, err := os.ReadFile(result.Path)
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(content), "# MONITOR CONFIG") assert.Contains(t, string(content), "# MONITOR CONFIG")
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty") assert.Contains(t, string(content), "bind = $mod, T, exec, $TERMINAL")
assert.Contains(t, string(content), "exec-once = ") assert.Contains(t, string(content), "exec-once = ")
}) })
@@ -471,7 +471,7 @@ general {
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144") assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60") assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty") assert.Contains(t, string(newContent), "bind = $mod, T, exec, $TERMINAL")
assert.NotContains(t, string(newContent), "monitor = eDP-2") assert.NotContains(t, string(newContent), "monitor = eDP-2")
}) })
} }
@@ -479,23 +479,22 @@ general {
func TestNiriConfigStructure(t *testing.T) { func TestNiriConfigStructure(t *testing.T) {
assert.Contains(t, NiriConfig, "input {") assert.Contains(t, NiriConfig, "input {")
assert.Contains(t, NiriConfig, "layout {") assert.Contains(t, NiriConfig, "layout {")
assert.Contains(t, NiriConfig, "binds {")
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}") assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
assert.Contains(t, NiriBindsConfig, "binds {")
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
} }
func TestHyprlandConfigStructure(t *testing.T) { func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG") assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
assert.Contains(t, HyprlandConfig, "# STARTUP APPS") assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG") assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS") assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}") assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}") assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, $TERMINAL")
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle") assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$") assert.Contains(t, HyprlandConfig, "windowrule {")
assert.Contains(t, HyprlandConfig, "match:class = ^(com\\.mitchellh\\.ghostty)$")
} }
func TestGhosttyConfigStructure(t *testing.T) { func TestGhosttyConfigStructure(t *testing.T) {

View File

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

View File

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

View File

@@ -0,0 +1,195 @@
binds {
// === System & Overview ===
Mod+D repeat=false { toggle-overview; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}

View File

@@ -0,0 +1,36 @@
layout {
background-color "transparent"
focus-ring {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
}
border {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
}
shadow {
color "#00000070"
}
tab-indicator {
active-color "#9dcbfb"
inactive-color "#8c9199"
urgent-color "#ffb4ab"
}
insert-hint {
color "#9dcbfb80"
}
}
recent-windows {
highlight {
active-color "#124a73"
urgent-color "#ffb4ab"
}
}

View File

@@ -0,0 +1,17 @@
layout {
gaps 4
border {
width 2
}
focus-ring {
width 2
}
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
tiled-state true
draw-border-with-background false
}

View File

@@ -116,15 +116,9 @@ overview {
// See the binds section below for more spawn examples. // See the binds section below for more spawn examples.
// This line starts waybar, a commonly used bar for Wayland compositors. // This line starts waybar, a commonly used bar for Wayland compositors.
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &" spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
spawn-at-startup "dms" "run"
spawn-at-startup "{{POLKIT_AGENT_PATH}}" spawn-at-startup "{{POLKIT_AGENT_PATH}}"
environment { environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
TERMINAL "{{TERMINAL_COMMAND}}"
} }
hotkey-overlay { hotkey-overlay {
skip-at-startup skip-at-startup
@@ -214,210 +208,27 @@ window-rule {
match app-id="zoom" match app-id="zoom"
open-floating true open-floating true
} }
window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
// Open dms windows as floating by default // Open dms windows as floating by default
window-rule { window-rule {
match app-id=r#"org.quickshell$"# match app-id=r#"org.quickshell$"#
open-floating true open-floating true
} }
binds {
// === System & Overview ===
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
Mod+Tab repeat=false { toggle-overview; }
Mod+Shift+Slash { show-hotkey-overlay; }
// === Application Launchers ===
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle";
}
Mod+M hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
Mod+Comma hotkey-overlay-title="Settings" {
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
}
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
}
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
// === Security ===
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
spawn "dms" "ipc" "call" "lock" "lock";
}
Mod+Shift+E { quit; }
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
}
// === Audio Controls ===
XF86AudioRaiseVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "increment" "3";
}
XF86AudioLowerVolume allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "decrement" "3";
}
XF86AudioMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "mute";
}
XF86AudioMicMute allow-when-locked=true {
spawn "dms" "ipc" "call" "audio" "micmute";
}
// === Brightness Controls ===
XF86MonBrightnessUp allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
}
XF86MonBrightnessDown allow-when-locked=true {
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
}
// === Window Management ===
Mod+Q repeat=false { close-window; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
Mod+Shift+T { toggle-window-floating; }
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
Mod+W { toggle-column-tabbed-display; }
// === Focus Navigation ===
Mod+Left { focus-column-left; }
Mod+Down { focus-window-down; }
Mod+Up { focus-window-up; }
Mod+Right { focus-column-right; }
Mod+H { focus-column-left; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+L { focus-column-right; }
// === Window Movement ===
Mod+Shift+Left { move-column-left; }
Mod+Shift+Down { move-window-down; }
Mod+Shift+Up { move-window-up; }
Mod+Shift+Right { move-column-right; }
Mod+Shift+H { move-column-left; }
Mod+Shift+J { move-window-down; }
Mod+Shift+K { move-window-up; }
Mod+Shift+L { move-column-right; }
// === Column Navigation ===
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+Ctrl+Home { move-column-to-first; }
Mod+Ctrl+End { move-column-to-last; }
// === Monitor Navigation ===
Mod+Ctrl+Left { focus-monitor-left; }
//Mod+Ctrl+Down { focus-monitor-down; }
//Mod+Ctrl+Up { focus-monitor-up; }
Mod+Ctrl+Right { focus-monitor-right; }
Mod+Ctrl+H { focus-monitor-left; }
Mod+Ctrl+J { focus-monitor-down; }
Mod+Ctrl+K { focus-monitor-up; }
Mod+Ctrl+L { focus-monitor-right; }
// === Move to Monitor ===
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
// === Workspace Navigation ===
Mod+Page_Down { focus-workspace-down; }
Mod+Page_Up { focus-workspace-up; }
Mod+U { focus-workspace-down; }
Mod+I { focus-workspace-up; }
Mod+Ctrl+Down { move-column-to-workspace-down; }
Mod+Ctrl+Up { move-column-to-workspace-up; }
Mod+Ctrl+U { move-column-to-workspace-down; }
Mod+Ctrl+I { move-column-to-workspace-up; }
// === Move Workspaces ===
Mod+Shift+Page_Down { move-workspace-down; }
Mod+Shift+Page_Up { move-workspace-up; }
Mod+Shift+U { move-workspace-down; }
Mod+Shift+I { move-workspace-up; }
// === Mouse Wheel Navigation ===
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+Ctrl+WheelScrollRight { move-column-right; }
Mod+Ctrl+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
// === Numbered Workspaces ===
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
// === Move to Numbered Workspaces ===
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
Mod+Shift+3 { move-column-to-workspace 3; }
Mod+Shift+4 { move-column-to-workspace 4; }
Mod+Shift+5 { move-column-to-workspace 5; }
Mod+Shift+6 { move-column-to-workspace 6; }
Mod+Shift+7 { move-column-to-workspace 7; }
Mod+Shift+8 { move-column-to-workspace 8; }
Mod+Shift+9 { move-column-to-workspace 9; }
// === Column Management ===
Mod+BracketLeft { consume-or-expel-window-left; }
Mod+BracketRight { consume-or-expel-window-right; }
Mod+Period { expel-window-from-column; }
// === Sizing & Layout ===
Mod+R { switch-preset-column-width; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+Ctrl+F { expand-column-to-available-width; }
Mod+C { center-column; }
Mod+Ctrl+C { center-visible-columns; }
// === Manual Sizing ===
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// === Screenshots ===
XF86Launch1 { screenshot; }
Ctrl+XF86Launch1 { screenshot-screen; }
Alt+XF86Launch1 { screenshot-window; }
Print { screenshot; }
Ctrl+Print { screenshot-screen; }
Alt+Print { screenshot-window; }
// === System Controls ===
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
Mod+Shift+P { power-off-monitors; }
}
debug { debug {
honor-xdg-activation-with-invalid-serial honor-xdg-activation-with-invalid-serial
} }
// Override to disable super+tab
recent-windows {
binds {
Alt+Tab { next-window scope="output"; }
Alt+Shift+Tab { previous-window scope="output"; }
Alt+grave { next-window filter="app-id"; }
Alt+Shift+grave { previous-window filter="app-id"; }
}
}
// Include dms files
include "dms/colors.kdl"
include "dms/layout.kdl"
include "dms/alttab.kdl"
include "dms/binds.kdl"

View File

@@ -4,3 +4,15 @@ import _ "embed"
//go:embed embedded/niri.kdl //go:embed embedded/niri.kdl
var NiriConfig string var NiriConfig string
//go:embed embedded/niri-colors.kdl
var NiriColorsConfig string
//go:embed embedded/niri-layout.kdl
var NiriLayoutConfig string
//go:embed embedded/niri-alttab.kdl
var NiriAlttabConfig string
//go:embed embedded/niri-binds.kdl
var NiriBindsConfig string

View File

@@ -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),
}
}

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectWindowManager(wm)) dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell()) dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectXDGPortal()) dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectPolkitAgent())
dependencies = append(dependencies, d.detectAccountsService()) dependencies = append(dependencies, d.detectAccountsService())
if wm == deps.WindowManagerNiri { if wm == deps.WindowManagerNiri {
@@ -70,7 +69,6 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectMatugen()) dependencies = append(dependencies, d.detectMatugen())
dependencies = append(dependencies, d.detectDgop()) dependencies = append(dependencies, d.detectDgop())
dependencies = append(dependencies, d.detectHyprpicker())
dependencies = append(dependencies, d.detectClipboardTools()...) dependencies = append(dependencies, d.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
@@ -90,20 +88,6 @@ func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
} }
} }
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if d.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency { func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if d.commandExists("xwayland-satellite") { if d.commandExists("xwayland-satellite") {
@@ -139,33 +123,65 @@ func (d *DebianDistribution) packageInstalled(pkg string) bool {
} }
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{ packages := map[string]PackageMapping{
// Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem}, "kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}, // DMS packages from OBS with variant support
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}, "dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"}, "quickshell": d.getQuickshellMapping(variants["quickshell"]),
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, "cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
} }
if wm == deps.WindowManagerNiri { if wm == deps.WindowManagerNiri {
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"} niriVariant := variants["niri"]
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"} packages["niri"] = d.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = d.getXwaylandSatelliteMapping(niriVariant)
} }
return packages return packages
} }
func (d *DebianDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
}
func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -238,8 +254,23 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
systemPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) systemPkgs, obsPkgs, manualPkgs, variantMap := d.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Enable OBS repositories
if len(obsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling OBS repositories...",
IsComplete: false,
LogOutput: "Setting up OBS repositories for additional packages",
}
if err := d.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable OBS repositories: %w", err)
}
}
// System Packages
if len(systemPkgs) > 0 { if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -254,6 +285,22 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
} }
} }
// OBS Packages
obsPkgNames := d.extractPackageNames(obsPkgs)
if len(obsPkgNames) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
}
if err := d.installAPTPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err)
}
}
// Manual Builds
if len(manualPkgs) > 0 { if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -286,6 +333,15 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := d.DetectTerminalFromDeps(dependencies)
if err := d.WriteEnvironmentConfig(terminal); err != nil {
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := d.EnableDMSService(ctx); err != nil {
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
Progress: 1.0, Progress: 1.0,
@@ -297,8 +353,9 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
return nil return nil
} }
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) { func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
systemPkgs := []string{} systemPkgs := []string{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{} manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant) variantMap := make(map[string]deps.PackageVariant)
@@ -306,7 +363,7 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
variantMap[dep.Name] = dep.Variant variantMap[dep.Name] = dep.Variant
} }
packageMap := d.GetPackageMapping(wm) packageMap := d.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies { for _, dep := range dependencies {
if disabledFlags[dep.Name] { if disabledFlags[dep.Name] {
@@ -326,12 +383,116 @@ func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency,
switch pkgInfo.Repository { switch pkgInfo.Repository {
case RepoTypeSystem: case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name) systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual: case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name) manualPkgs = append(manualPkgs, dep.Name)
} }
} }
return systemPkgs, manualPkgs, variantMap return systemPkgs, obsPkgs, manualPkgs, variantMap
}
func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
osInfo, err := GetOSInfo()
if err != nil {
return fmt.Errorf("failed to get OS info: %w", err)
}
// Determine Debian version for OBS repository URL
debianVersion := "Debian_13"
if osInfo.VersionID == "testing" {
debianVersion = "Debian_Testing"
}
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, debianVersion)
// Check if repository already exists
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
checkCmd := exec.CommandContext(ctx, "test", "-f", listFile)
if checkCmd.Run() == nil {
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
continue
}
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
// Create keyrings directory if it doesn't exist
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
if err := mkdirCmd.Run(); err != nil {
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding OBS GPG key for %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
}
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
}
// Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Adding OBS repository %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
}
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
d.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
}
}
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Updating package lists...",
NeedsSudo: true,
CommandInfo: "sudo apt-get update",
}
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
}
}
return nil
} }
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -494,30 +655,6 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90) return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
} }
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
d.log("Installing Ghostty using Debian installer script...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Running Ghostty Debian installer...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
LogOutput: "Installing Ghostty using pre-built Debian package",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err)
}
d.log("Ghostty installed successfully using Debian installer")
return nil
}
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
@@ -527,10 +664,6 @@ func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages
for _, pkg := range packages { for _, pkg := range packages {
switch pkg { switch pkg {
case "ghostty":
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
default: default:
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err) return fmt.Errorf("failed to install %s: %w", pkg, err)

View File

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

View File

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

View File

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

View File

@@ -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...")

View File

@@ -66,7 +66,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectWindowManager(wm)) dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell()) dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectXDGPortal()) dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectPolkitAgent())
dependencies = append(dependencies, o.detectAccountsService()) dependencies = append(dependencies, o.detectAccountsService())
// Hyprland-specific tools // Hyprland-specific tools
@@ -82,7 +81,6 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
// Base detections (common across distros) // Base detections (common across distros)
dependencies = append(dependencies, o.detectMatugen()) dependencies = append(dependencies, o.detectMatugen())
dependencies = append(dependencies, o.detectDgop()) dependencies = append(dependencies, o.detectDgop())
dependencies = append(dependencies, o.detectHyprpicker())
dependencies = append(dependencies, o.detectClipboardTools()...) dependencies = append(dependencies, o.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
@@ -102,20 +100,6 @@ func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
} }
} }
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if o.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool { func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("rpm", "-q", pkg) cmd := exec.Command("rpm", "-q", pkg)
err := cmd.Run() err := cmd.Run()
@@ -135,34 +119,59 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem}, "cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
// Manual builds // DMS packages from OBS
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}, "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, "quickshell": o.getQuickshellMapping(variants["quickshell"]),
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
} }
switch wm { switch wm {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem} packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem} // Niri stable has native package support on openSUSE
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem} niriVariant := variants["niri"]
packages["niri"] = o.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = o.getXwaylandSatelliteMapping(niriVariant)
} }
return packages return packages
} }
func (o *OpenSUSEDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
}
func (o *OpenSUSEDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
func (o *OpenSUSEDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
}
func (o *OpenSUSEDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
}
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency { func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if o.commandExists("xwayland-satellite") { if o.commandExists("xwayland-satellite") {
@@ -294,9 +303,23 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
systemPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) systemPkgs, obsPkgs, manualPkgs, variantMap := o.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
// Phase 2: System Packages (Zypper) // Enable OBS repositories
if len(obsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling OBS repositories...",
IsComplete: false,
LogOutput: "Setting up OBS repositories for additional packages",
}
if err := o.enableOBSRepos(ctx, obsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable OBS repositories: %w", err)
}
}
// Phase 3: System Packages (Zypper)
if len(systemPkgs) > 0 { if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -311,7 +334,22 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
} }
} }
// Phase 3: Manual Builds // OBS Packages
obsPkgNames := o.extractPackageNames(obsPkgs)
if len(obsPkgNames) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.65,
Step: fmt.Sprintf("Installing %d OBS packages...", len(obsPkgNames)),
IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
}
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err)
}
}
// Manual Builds
if len(manualPkgs) > 0 { if len(manualPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -325,7 +363,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
} }
} }
// Phase 4: Configuration // Configuration
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration, Phase: PhaseConfiguration,
Progress: 0.90, Progress: 0.90,
@@ -334,7 +372,16 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
// Phase 5: Complete terminal := o.DetectTerminalFromDeps(dependencies)
if err := o.WriteEnvironmentConfig(terminal); err != nil {
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := o.EnableDMSService(ctx); err != nil {
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Complete
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
Progress: 1.0, Progress: 1.0,
@@ -346,8 +393,9 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
return nil return nil
} }
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []string, map[string]deps.PackageVariant) { func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping, []string, map[string]deps.PackageVariant) {
systemPkgs := []string{} systemPkgs := []string{}
obsPkgs := []PackageMapping{}
manualPkgs := []string{} manualPkgs := []string{}
variantMap := make(map[string]deps.PackageVariant) variantMap := make(map[string]deps.PackageVariant)
@@ -375,12 +423,80 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
switch pkgInfo.Repository { switch pkgInfo.Repository {
case RepoTypeSystem: case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name) systemPkgs = append(systemPkgs, pkgInfo.Name)
case RepoTypeOBS:
obsPkgs = append(obsPkgs, pkgInfo)
case RepoTypeManual: case RepoTypeManual:
manualPkgs = append(manualPkgs, dep.Name) manualPkgs = append(manualPkgs, dep.Name)
} }
} }
return systemPkgs, manualPkgs, variantMap return systemPkgs, obsPkgs, manualPkgs, variantMap
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
for _, pkg := range obsPkgs {
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
// RepoURL format: "home:AvengeMedia:danklinux"
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
repoPath, pkg.RepoURL)
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
if checkCmd.Run() == nil {
o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.20,
Step: fmt.Sprintf("Enabling OBS repo %s...", pkg.RepoURL),
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
}
cmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
o.log(fmt.Sprintf("OBS repo %s enabled successfully", pkg.RepoURL))
}
}
// Refresh repositories with GPG auto-import
if len(enabledRepos) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Refreshing repositories...",
NeedsSudo: true,
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
}
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to refresh repositories: %w", err)
}
}
return nil
} }
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {

View File

@@ -3,9 +3,7 @@ package distros
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -66,7 +64,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, u.detectWindowManager(wm)) dependencies = append(dependencies, u.detectWindowManager(wm))
dependencies = append(dependencies, u.detectQuickshell()) dependencies = append(dependencies, u.detectQuickshell())
dependencies = append(dependencies, u.detectXDGPortal()) dependencies = append(dependencies, u.detectXDGPortal())
dependencies = append(dependencies, u.detectPolkitAgent())
dependencies = append(dependencies, u.detectAccountsService()) dependencies = append(dependencies, u.detectAccountsService())
// Hyprland-specific tools // Hyprland-specific tools
@@ -82,7 +79,6 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
// Base detections (common across distros) // Base detections (common across distros)
dependencies = append(dependencies, u.detectMatugen()) dependencies = append(dependencies, u.detectMatugen())
dependencies = append(dependencies, u.detectDgop()) dependencies = append(dependencies, u.detectDgop())
dependencies = append(dependencies, u.detectHyprpicker())
dependencies = append(dependencies, u.detectClipboardTools()...) dependencies = append(dependencies, u.detectClipboardTools()...)
return dependencies, nil return dependencies, nil
@@ -102,20 +98,6 @@ func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
} }
} }
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
status := deps.StatusMissing
if u.packageInstalled("mate-polkit") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "mate-polkit",
Status: status,
Description: "PolicyKit authentication agent",
Required: true,
}
}
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency { func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if u.commandExists("xwayland-satellite") { if u.commandExists("xwayland-satellite") {
@@ -151,6 +133,10 @@ func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
} }
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return u.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{ packages := map[string]PackageMapping{
// Standard APT packages // Standard APT packages
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
@@ -158,37 +144,60 @@ func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem}, "alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem}, "wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem}, "xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem}, "accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
// Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs) // DMS packages from PPAs
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}, "dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}, "quickshell": u.getQuickshellMapping(variants["quickshell"]),
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"}, "matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"}, "dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"}, "cliphist": {Name: "cliphist", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"}, "ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
} }
switch wm { switch wm {
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
// Use the cppiber PPA for Hyprland // Use the cppiber PPA for Hyprland
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"} packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"} packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem} packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"} niriVariant := variants["niri"]
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"} packages["niri"] = u.getNiriMapping(niriVariant)
packages["xwayland-satellite"] = u.getXwaylandSatelliteMapping(niriVariant)
} }
return packages return packages
} }
func (u *UbuntuDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "dms-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms-git"}
}
return PackageMapping{Name: "dms", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms"}
}
func (u *UbuntuDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
if forceQuickshellGit || variant == deps.VariantGit {
return PackageMapping{Name: "quickshell-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
return PackageMapping{Name: "quickshell", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
func (u *UbuntuDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "niri-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
return PackageMapping{Name: "niri", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
func (u *UbuntuDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantGit {
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
}
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -343,6 +352,15 @@ func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies [
LogOutput: "Starting post-installation configuration...", LogOutput: "Starting post-installation configuration...",
} }
terminal := u.DetectTerminalFromDeps(dependencies)
if err := u.WriteEnvironmentConfig(terminal); err != nil {
u.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
}
if err := u.EnableDMSService(ctx); err != nil {
u.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
}
// Phase 7: Complete // Phase 7: Complete
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseComplete, Phase: PhaseComplete,
@@ -365,7 +383,7 @@ func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency,
variantMap[dep.Name] = dep.Variant variantMap[dep.Name] = dep.Variant
} }
packageMap := u.GetPackageMapping(wm) packageMap := u.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies { for _, dep := range dependencies {
if disabledFlags[dep.Name] { if disabledFlags[dep.Name] {
@@ -545,10 +563,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
buildDeps["libxcb1-dev"] = true buildDeps["libxcb1-dev"] = true
buildDeps["libpipewire-0.3-dev"] = true buildDeps["libpipewire-0.3-dev"] = true
buildDeps["libpam0g-dev"] = true buildDeps["libpam0g-dev"] = true
case "ghostty":
buildDeps["curl"] = true
buildDeps["libgtk-4-dev"] = true
buildDeps["libadwaita-1-dev"] = true
case "matugen": case "matugen":
buildDeps["curl"] = true buildDeps["curl"] = true
case "cliphist": case "cliphist":
@@ -562,10 +576,6 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil { if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Rust: %w", err) return fmt.Errorf("failed to install Rust: %w", err)
} }
case "ghostty":
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Zig: %w", err)
}
case "cliphist", "dgop": case "cliphist", "dgop":
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil { if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install Go: %w", err) return fmt.Errorf("failed to install Go: %w", err)
@@ -629,40 +639,6 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
return nil return nil
} }
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if u.commandExists("zig") {
return nil
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
return fmt.Errorf("failed to download Zig: %w", err)
}
extractCmd := ExecSudoCommand(ctx, sudoPassword,
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
return fmt.Errorf("failed to extract Zig: %w", err)
}
linkCmd := ExecSudoCommand(ctx, sudoPassword,
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
}
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if u.commandExists("go") { if u.commandExists("go") {
return nil return nil
@@ -710,30 +686,6 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90) return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
} }
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
u.log("Installing Ghostty using Ubuntu installer script...")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.1,
Step: "Running Ghostty Ubuntu installer...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
}
installCmd := ExecSudoCommand(ctx, sudoPassword,
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err)
}
u.log("Ghostty installed successfully using Ubuntu installer")
return nil
}
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, variantMap map[string]deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
@@ -743,10 +695,6 @@ func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages
for _, pkg := range packages { for _, pkg := range packages {
switch pkg { switch pkg {
case "ghostty":
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install ghostty: %w", err)
}
default: default:
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil { if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, variantMap, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install %s: %w", pkg, err) return fmt.Errorf("failed to install %s: %w", pkg, err)

View File

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

View File

@@ -333,35 +333,6 @@ func (n *NiriProvider) isRecentWindowsAction(action string) bool {
} }
} }
func (n *NiriProvider) parseSpawnArgs(s string) []string {
var args []string
var current strings.Builder
var inQuote, escaped bool
for _, r := range s {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 {
args = append(args, current.String())
current.Reset()
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 {
args = append(args, current.String())
}
return args
}
func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node { func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
node := document.NewNode() node := document.NewNode()
node.SetName(bind.Key) node.SetName(bind.Key)
@@ -392,19 +363,62 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
action = strings.TrimSpace(action) action = strings.TrimSpace(action)
node := document.NewNode() node := document.NewNode()
if !strings.HasPrefix(action, "spawn ") { parts := n.parseActionParts(action)
if len(parts) == 0 {
node.SetName(action) node.SetName(action)
return node return node
} }
node.SetName("spawn") node.SetName(parts[0])
args := n.parseSpawnArgs(strings.TrimPrefix(action, "spawn ")) for _, arg := range parts[1:] {
for _, arg := range args { if strings.Contains(arg, "=") {
kv := strings.SplitN(arg, "=", 2)
switch kv[1] {
case "true":
node.AddProperty(kv[0], true, "")
case "false":
node.AddProperty(kv[0], false, "")
default:
node.AddProperty(kv[0], kv[1], "")
}
continue
}
node.AddArgument(arg, "") node.AddArgument(arg, "")
} }
return node return node
} }
func (n *NiriProvider) parseActionParts(action string) []string {
var parts []string
var current strings.Builder
var inQuote, escaped, wasQuoted bool
for _, r := range action {
switch {
case escaped:
current.WriteRune(r)
escaped = false
case r == '\\':
escaped = true
case r == '"':
wasQuoted = true
inQuote = !inQuote
case r == ' ' && !inQuote:
if current.Len() > 0 || wasQuoted {
parts = append(parts, current.String())
current.Reset()
wasQuoted = false
}
default:
current.WriteRune(r)
}
}
if current.Len() > 0 || wasQuoted {
parts = append(parts, current.String())
}
return parts
}
func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error { func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error {
overridePath := n.GetOverridePath() overridePath := n.GetOverridePath()
content := n.generateBindsContent(binds) content := n.generateBindsContent(binds)
@@ -501,21 +515,50 @@ func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, in
sb.WriteString(" { ") sb.WriteString(" { ")
if len(node.Children) > 0 { if len(node.Children) > 0 {
child := node.Children[0] child := node.Children[0]
sb.WriteString(child.Name.String()) actionName := child.Name.String()
sb.WriteString(actionName)
forceQuote := actionName == "spawn"
for _, arg := range child.Arguments { for _, arg := range child.Arguments {
sb.WriteString(" ") sb.WriteString(" ")
n.writeQuotedArg(sb, arg.ValueString()) n.writeArg(sb, arg.ValueString(), forceQuote)
}
if child.Properties.Exist() {
sb.WriteString(" ")
sb.WriteString(strings.TrimLeft(child.Properties.String(), " "))
} }
} }
sb.WriteString("; }\n") sb.WriteString("; }\n")
} }
func (n *NiriProvider) writeQuotedArg(sb *strings.Builder, val string) { func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) {
if !forceQuote && n.isNumericArg(val) {
sb.WriteString(val)
return
}
sb.WriteString("\"") sb.WriteString("\"")
sb.WriteString(strings.ReplaceAll(val, "\"", "\\\"")) sb.WriteString(strings.ReplaceAll(val, "\"", "\\\""))
sb.WriteString("\"") sb.WriteString("\"")
} }
func (n *NiriProvider) isNumericArg(val string) bool {
if val == "" {
return false
}
start := 0
if val[0] == '-' || val[0] == '+' {
if len(val) == 1 {
return false
}
start = 1
}
for i := start; i < len(val); i++ {
if val[i] < '0' || val[i] > '9' {
return false
}
}
return true
}
func (n *NiriProvider) validateBindsContent(content string) error { func (n *NiriProvider) validateBindsContent(content string) error {
tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl") tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl")
if err != nil { if err != nil {

View File

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

View File

@@ -496,3 +496,135 @@ func TestNiriParseMultipleArgs(t *testing.T) {
} }
} }
} }
func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
Mod+2 hotkey-overlay-title="Focus Workspace 2" { focus-workspace 2; }
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Key {
case "1":
if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" {
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "1" {
t.Errorf("Mod+1 action/args mismatch: %+v", kb)
}
if kb.Description != "Focus Workspace 1" {
t.Errorf("Mod+1 description = %q, want 'Focus Workspace 1'", kb.Description)
}
}
case "0":
if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "10" {
t.Errorf("Mod+0 action/args mismatch: %+v", kb)
}
}
}
}
func TestNiriParseQuotedStringArgs(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
if kb.Action == "set-column-width" {
if len(kb.Args) != 1 {
t.Errorf("set-column-width should have 1 arg, got %d", len(kb.Args))
continue
}
if kb.Args[0] != "-10%" && kb.Args[0] != "+10%" {
t.Errorf("set-column-width arg = %q, want -10%% or +10%%", kb.Args[0])
}
}
}
}
func TestNiriParseActionWithProperties(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
content := `binds {
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; }
Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; }
Alt+Tab { next-window scope="output"; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
if len(result.Section.Keybinds) != 3 {
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
}
for _, kb := range result.Section.Keybinds {
switch kb.Action {
case "move-column-to-workspace":
if len(kb.Args) != 2 {
t.Errorf("move-column-to-workspace should have 2 args (index + focus), got %d", len(kb.Args))
}
hasIndex := false
hasFocus := false
for _, arg := range kb.Args {
if arg == "1" || arg == "2" {
hasIndex = true
}
if arg == "focus=false" {
hasFocus = true
}
}
if !hasIndex {
t.Errorf("move-column-to-workspace missing index arg")
}
if !hasFocus {
t.Errorf("move-column-to-workspace missing focus=false arg")
}
case "next-window":
if kb.Key != "Tab" {
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
}
}
}
}

View File

@@ -397,3 +397,211 @@ recent-windows {
t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"])) t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"]))
} }
} }
func TestNiriGenerateBindsContentNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
tests := []struct {
name string
binds map[string]*overrideBind
expected string
}{
{
name: "workspace with numeric arg",
binds: map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
},
expected: `binds {
Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; }
}
`,
},
{
name: "workspace with large numeric arg",
binds: map[string]*overrideBind{
"Mod+0": {
Key: "Mod+0",
Action: "focus-workspace 10",
Description: "Focus Workspace 10",
},
},
expected: `binds {
Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; }
}
`,
},
{
name: "percentage string arg (should be quoted)",
binds: map[string]*overrideBind{
"Super+Minus": {
Key: "Super+Minus",
Action: `set-column-width "-10%"`,
Description: "Adjust Column Width -10%",
},
},
expected: `binds {
Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; }
}
`,
},
{
name: "positive percentage string arg",
binds: map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: `set-column-width "+10%"`,
Description: "Adjust Column Width +10%",
},
},
expected: `binds {
Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; }
}
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := provider.generateBindsContent(tt.binds)
if result != tt.expected {
t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected)
}
})
}
}
func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Super+Equal": {
Key: "Super+Equal",
Action: "set-window-height +10%",
Description: "Adjust Window Height +10%",
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: `spawn "dms" "ipc" "call" "audio" "decrement" "3"`,
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"XF86AudioLowerVolume": {
Key: "XF86AudioLowerVolume",
Action: "spawn dms ipc call audio decrement 3",
Options: map[string]any{"allow-when-locked": true},
},
}
content := provider.generateBindsContent(binds)
expected := `binds {
XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; }
}
`
if content != expected {
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
}
}
func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
provider := NewNiriProvider("")
binds := map[string]*overrideBind{
"Mod+1": {
Key: "Mod+1",
Action: "focus-workspace 1",
Description: "Focus Workspace 1",
},
"Mod+2": {
Key: "Mod+2",
Action: "focus-workspace 2",
Description: "Focus Workspace 2",
},
"Mod+Shift+1": {
Key: "Mod+Shift+1",
Action: "move-column-to-workspace 1",
Description: "Move to Workspace 1",
},
"Super+Minus": {
Key: "Super+Minus",
Action: "set-column-width -10%",
Description: "Adjust Column Width -10%",
},
}
content := provider.generateBindsContent(binds)
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content)
}
if len(result.Section.Keybinds) != 4 {
t.Errorf("Expected 4 keybinds after round-trip, got %d", len(result.Section.Keybinds))
}
foundFocusWS1 := false
foundMoveWS1 := false
foundSetWidth := false
for _, kb := range result.Section.Keybinds {
switch {
case kb.Action == "focus-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundFocusWS1 = true
case kb.Action == "move-column-to-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1":
foundMoveWS1 = true
case kb.Action == "set-column-width" && len(kb.Args) > 0 && kb.Args[0] == "-10%":
foundSetWidth = true
}
}
if !foundFocusWS1 {
t.Error("focus-workspace 1 not found after round-trip")
}
if !foundMoveWS1 {
t.Error("move-column-to-workspace 1 not found after round-trip")
}
if !foundSetWidth {
t.Error("set-column-width -10% not found after round-trip")
}
}

View File

@@ -0,0 +1,587 @@
package matugen
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
var (
matugenVersionOnce sync.Once
matugenSupportsCOE bool
)
type Options struct {
StateDir string
ShellDir string
ConfigDir string
Kind string
Value string
Mode string
IconTheme string
MatugenType string
RunUserTemplates bool
StockColors string
SyncModeWithPortal bool
TerminalsAlwaysDark bool
}
type ColorsOutput struct {
Colors struct {
Dark map[string]string `json:"dark"`
Light map[string]string `json:"light"`
} `json:"colors"`
}
func (o *Options) ColorsOutput() string {
return filepath.Join(o.StateDir, "dms-colors.json")
}
func Run(opts Options) error {
if opts.StateDir == "" {
return fmt.Errorf("state-dir is required")
}
if opts.ShellDir == "" {
return fmt.Errorf("shell-dir is required")
}
if opts.ConfigDir == "" {
return fmt.Errorf("config-dir is required")
}
if opts.Kind == "" {
return fmt.Errorf("kind is required")
}
if opts.Value == "" {
return fmt.Errorf("value is required")
}
if opts.Mode == "" {
opts.Mode = "dark"
}
if opts.MatugenType == "" {
opts.MatugenType = "scheme-tonal-spot"
}
if opts.IconTheme == "" {
opts.IconTheme = "System Default"
}
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
return fmt.Errorf("failed to create state dir: %w", err)
}
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
if err := buildOnce(&opts); err != nil {
return err
}
if opts.SyncModeWithPortal {
syncColorScheme(opts.Mode)
}
log.Info("Done")
return nil
}
func buildOnce(opts *Options) error {
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
if err != nil {
return fmt.Errorf("failed to create temp config: %w", err)
}
defer os.Remove(cfgFile.Name())
defer cfgFile.Close()
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
return fmt.Errorf("failed to build config: %w", err)
}
cfgFile.Close()
var primaryDark, primaryLight, surface string
var dank16JSON string
var importArgs []string
if opts.StockColors != "" {
log.Info("Using stock/custom theme colors with matugen base")
primaryDark = extractNestedColor(opts.StockColors, "primary", "dark")
primaryLight = extractNestedColor(opts.StockColors, "primary", "light")
surface = extractNestedColor(opts.StockColors, "surface", "dark")
if primaryDark == "" {
return fmt.Errorf("failed to extract primary dark from stock colors")
}
if primaryLight == "" {
primaryLight = primaryDark
}
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
importData := fmt.Sprintf(`{"colors": %s, "dank16": %s}`, opts.StockColors, dank16JSON)
importArgs = []string{"--import-json-string", importData}
log.Info("Running matugen color hex with stock color overrides")
args := []string{"color", "hex", primaryDark, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
}
} else {
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
matJSON, err := runMatugenDryRun(opts)
if err != nil {
return fmt.Errorf("matugen dry-run failed: %w", err)
}
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
primaryLight = extractMatugenColor(matJSON, "primary", "light")
surface = extractMatugenColor(matJSON, "surface", "dark")
if primaryDark == "" {
return fmt.Errorf("failed to extract primary color")
}
if primaryLight == "" {
primaryLight = primaryDark
}
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
importData := fmt.Sprintf(`{"dank16": %s}`, dank16JSON)
importArgs = []string{"--import-json-string", importData}
log.Infof("Running matugen %s with dank16 injection", opts.Kind)
var args []string
switch opts.Kind {
case "hex":
args = []string{"color", "hex", opts.Value}
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", opts.Mode, "-t", opts.MatugenType, "-c", cfgFile.Name())
args = append(args, importArgs...)
if err := runMatugen(args); err != nil {
return err
}
}
refreshGTK(opts.ConfigDir, opts.Mode)
signalTerminals()
return nil
}
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
wroteConfig := false
if opts.RunUserTemplates {
if data, err := os.ReadFile(userConfigPath); err == nil {
configSection := extractTOMLSection(string(data), "[config]", "[templates]")
if configSection != "" {
cfgFile.WriteString(configSection)
cfgFile.WriteString("\n")
wroteConfig = true
}
}
}
if !wroteConfig {
cfgFile.WriteString("[config]\n\n")
}
baseConfigPath := filepath.Join(opts.ShellDir, "matugen", "configs", "base.toml")
if data, err := os.ReadFile(baseConfigPath); err == nil {
content := string(data)
lines := strings.Split(content, "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "[config]" {
continue
}
cfgFile.WriteString(substituteShellDir(line, opts.ShellDir) + "\n")
}
cfgFile.WriteString("\n")
}
fmt.Fprintf(cfgFile, `[templates.dank]
input_path = '%s/matugen/templates/dank.json'
output_path = '%s'
`, opts.ShellDir, opts.ColorsOutput())
switch opts.Mode {
case "light":
appendConfig(opts, cfgFile, "skip", "gtk3-light.toml")
default:
appendConfig(opts, cfgFile, "skip", "gtk3-dark.toml")
}
appendConfig(opts, cfgFile, "niri", "niri.toml")
appendConfig(opts, cfgFile, "qt5ct", "qt5ct.toml")
appendConfig(opts, cfgFile, "qt6ct", "qt6ct.toml")
appendConfig(opts, cfgFile, "firefox", "firefox.toml")
appendConfig(opts, cfgFile, "pywalfox", "pywalfox.toml")
appendConfig(opts, cfgFile, "vesktop", "vesktop.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "ghostty", "ghostty.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "kitty", "kitty.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "foot", "foot.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "alacritty", "alacritty.toml")
appendTerminalConfig(opts, cfgFile, tmpDir, "wezterm", "wezterm.toml")
appendConfig(opts, cfgFile, "dgop", "dgop.toml")
homeDir, _ := os.UserHomeDir()
appendVSCodeConfig(cfgFile, "vscode", filepath.Join(homeDir, ".vscode/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codium", filepath.Join(homeDir, ".vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "codeoss", filepath.Join(homeDir, ".config/Code - OSS/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions/local.dynamic-base16-dankshell-0.0.1"), opts.ShellDir)
if opts.RunUserTemplates {
if data, err := os.ReadFile(userConfigPath); err == nil {
templatesSection := extractTOMLSection(string(data), "[templates]", "")
if templatesSection != "" {
cfgFile.WriteString(templatesSection)
cfgFile.WriteString("\n")
}
}
}
userPluginConfigDir := filepath.Join(opts.ConfigDir, "matugen", "dms", "configs")
if entries, err := os.ReadDir(userPluginConfigDir); err == nil {
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".toml") {
continue
}
if data, err := os.ReadFile(filepath.Join(userPluginConfigDir, entry.Name())); err == nil {
cfgFile.WriteString(string(data))
cfgFile.WriteString("\n")
}
}
}
return nil
}
func appendConfig(opts *Options, cfgFile *os.File, checkCmd, fileName string) {
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
if _, err := os.Stat(configPath); err != nil {
return
}
if checkCmd != "skip" && !commandExists(checkCmd) {
return
}
data, err := os.ReadFile(configPath)
if err != nil {
return
}
cfgFile.WriteString(substituteShellDir(string(data), opts.ShellDir))
cfgFile.WriteString("\n")
}
func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir, checkCmd, fileName string) {
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
if _, err := os.Stat(configPath); err != nil {
return
}
if checkCmd != "skip" && !commandExists(checkCmd) {
return
}
data, err := os.ReadFile(configPath)
if err != nil {
return
}
content := string(data)
if !opts.TerminalsAlwaysDark {
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
cfgFile.WriteString("\n")
return
}
lines := strings.Split(content, "\n")
for _, line := range lines {
if !strings.Contains(line, "input_path") || !strings.Contains(line, "SHELL_DIR/matugen/templates/") {
continue
}
start := strings.Index(line, "'SHELL_DIR/matugen/templates/")
if start == -1 {
continue
}
end := strings.Index(line[start+1:], "'")
if end == -1 {
continue
}
templateName := line[start+len("'SHELL_DIR/matugen/templates/") : start+1+end]
origPath := filepath.Join(opts.ShellDir, "matugen", "templates", templateName)
origData, err := os.ReadFile(origPath)
if err != nil {
continue
}
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
tmpPath := filepath.Join(tmpDir, templateName)
if err := os.WriteFile(tmpPath, []byte(modified), 0644); err != nil {
continue
}
content = strings.ReplaceAll(content,
fmt.Sprintf("'SHELL_DIR/matugen/templates/%s'", templateName),
fmt.Sprintf("'%s'", tmpPath))
}
cfgFile.WriteString(substituteShellDir(content, opts.ShellDir))
cfgFile.WriteString("\n")
}
func appendVSCodeConfig(cfgFile *os.File, name, extDir, shellDir string) {
if _, err := os.Stat(extDir); err != nil {
return
}
templateDir := filepath.Join(shellDir, "matugen", "templates")
fmt.Fprintf(cfgFile, `[templates.dms%sdefault]
input_path = '%s/vscode-color-theme-default.json'
output_path = '%s/themes/dankshell-default.json'
[templates.dms%sdark]
input_path = '%s/vscode-color-theme-dark.json'
output_path = '%s/themes/dankshell-dark.json'
[templates.dms%slight]
input_path = '%s/vscode-color-theme-light.json'
output_path = '%s/themes/dankshell-light.json'
`, name, templateDir, extDir,
name, templateDir, extDir,
name, templateDir, extDir)
log.Infof("Added %s theme config (extension found at %s)", name, extDir)
}
func substituteShellDir(content, shellDir string) string {
return strings.ReplaceAll(content, "'SHELL_DIR/", "'"+shellDir+"/")
}
func extractTOMLSection(content, startMarker, endMarker string) string {
startIdx := strings.Index(content, startMarker)
if startIdx == -1 {
return ""
}
if endMarker == "" {
return content[startIdx:]
}
endIdx := strings.Index(content[startIdx:], endMarker)
if endIdx == -1 {
return content[startIdx:]
}
return content[startIdx : startIdx+endIdx]
}
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
func checkMatugenVersion() {
matugenVersionOnce.Do(func() {
cmd := exec.Command("matugen", "--version")
output, err := cmd.Output()
if err != nil {
return
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
})
}
func runMatugen(args []string) error {
checkMatugenVersion()
if matugenSupportsCOE {
args = append([]string{"--continue-on-error"}, args...)
}
cmd := exec.Command("matugen", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func runMatugenDryRun(opts *Options) (string, error) {
var args []string
switch opts.Kind {
case "hex":
args = []string{"color", "hex", opts.Value}
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
cmd := exec.Command("matugen", args...)
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.ReplaceAll(string(output), "\n", ""), nil
}
func extractMatugenColor(jsonStr, colorName, variant string) string {
var data map[string]any
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return ""
}
colors, ok := data["colors"].(map[string]any)
if !ok {
return ""
}
colorData, ok := colors[colorName].(map[string]any)
if !ok {
return ""
}
variantData, ok := colorData[variant].(string)
if !ok {
return ""
}
return variantData
}
func extractNestedColor(jsonStr, colorName, variant string) string {
var data map[string]any
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return ""
}
colorData, ok := data[colorName].(map[string]any)
if !ok {
return ""
}
variantData, ok := colorData[variant].(map[string]any)
if !ok {
return ""
}
color, ok := variantData["color"].(string)
if !ok {
return ""
}
return color
}
func generateDank16Variants(primaryDark, primaryLight, surface, mode string) string {
variantOpts := dank16.VariantOptions{
PrimaryDark: primaryDark,
PrimaryLight: primaryLight,
Background: surface,
UseDPS: true,
IsLightMode: mode == "light",
}
variantColors := dank16.GenerateVariantPalette(variantOpts)
return dank16.GenerateVariantJSON(variantColors)
}
func refreshGTK(configDir, mode string) {
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
info, err := os.Lstat(gtkCSS)
if err != nil {
return
}
shouldRun := false
if info.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(gtkCSS)
if err == nil && strings.Contains(target, "dank-colors.css") {
shouldRun = true
}
} else {
data, err := os.ReadFile(gtkCSS)
if err == nil && strings.Contains(string(data), "dank-colors.css") {
shouldRun = true
}
}
if !shouldRun {
return
}
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "adw-gtk3-"+mode).Run()
}
func signalTerminals() {
signalByName("kitty", syscall.SIGUSR1)
signalByName("ghostty", syscall.SIGUSR2)
signalByName(".kitty-wrapped", syscall.SIGUSR1)
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
}
func signalByName(name string, sig syscall.Signal) {
entries, err := os.ReadDir("/proc")
if err != nil {
return
}
for _, entry := range entries {
pid, err := strconv.Atoi(entry.Name())
if err != nil {
continue
}
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
if err != nil {
continue
}
if strings.TrimSpace(string(comm)) == name {
syscall.Kill(pid, sig)
}
}
}
func syncColorScheme(mode string) {
scheme := "prefer-dark"
if mode == "light" {
scheme = "default"
}
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
}
}

View File

@@ -0,0 +1,139 @@
package matugen
import (
"context"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
type Result struct {
Success bool
Error error
}
type QueuedJob struct {
Options Options
Done chan Result
Ctx context.Context
Cancel context.CancelFunc
}
type Queue struct {
mu sync.Mutex
current *QueuedJob
pending *QueuedJob
jobDone chan struct{}
}
var globalQueue *Queue
var queueOnce sync.Once
func GetQueue() *Queue {
queueOnce.Do(func() {
globalQueue = &Queue{
jobDone: make(chan struct{}, 1),
}
})
return globalQueue
}
func (q *Queue) Submit(opts Options) <-chan Result {
result := make(chan Result, 1)
ctx, cancel := context.WithCancel(context.Background())
job := &QueuedJob{
Options: opts,
Done: result,
Ctx: ctx,
Cancel: cancel,
}
q.mu.Lock()
if q.pending != nil {
log.Info("Cancelling pending theme request")
q.pending.Cancel()
q.pending.Done <- Result{Success: false, Error: context.Canceled}
close(q.pending.Done)
}
if q.current != nil {
q.pending = job
q.mu.Unlock()
log.Info("Theme request queued (worker running)")
return result
}
q.current = job
q.mu.Unlock()
go q.runWorker()
return result
}
func (q *Queue) runWorker() {
for {
q.mu.Lock()
job := q.current
if job == nil {
q.mu.Unlock()
return
}
q.mu.Unlock()
select {
case <-job.Ctx.Done():
q.finishJob(Result{Success: false, Error: context.Canceled})
continue
default:
}
log.Infof("Processing theme: %s %s (%s)", job.Options.Kind, job.Options.Value, job.Options.Mode)
err := Run(job.Options)
var result Result
if err != nil {
result = Result{Success: false, Error: err}
} else {
result = Result{Success: true}
}
q.finishJob(result)
}
}
func (q *Queue) finishJob(result Result) {
q.mu.Lock()
defer q.mu.Unlock()
if q.current != nil {
select {
case q.current.Done <- result:
default:
}
close(q.current.Done)
}
q.current = q.pending
q.pending = nil
if q.current == nil {
select {
case q.jobDone <- struct{}{}:
default:
}
}
}
func (q *Queue) IsRunning() bool {
q.mu.Lock()
defer q.mu.Unlock()
return q.current != nil
}
func (q *Queue) HasPending() bool {
q.mu.Lock()
defer q.mu.Unlock()
return q.pending != nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="keyboard_shortcuts_inhibit_unstable_v1">
<copyright>
Copyright © 2017 Red Hat Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="Protocol for inhibiting the compositor keyboard shortcuts">
This protocol specifies a way for a client to request the compositor
to ignore its own keyboard shortcuts for a given seat, so that all
key events from that seat get forwarded to a surface.
Warning! The protocol described in this file is experimental and
backward incompatible changes may be made. Backward compatible
changes may be added together with the corresponding interface
version bump.
Backward incompatible changes are done by bumping the version
number in the protocol and interface names and resetting the
interface version. Once the protocol is to be declared stable,
the 'z' prefix and the version number in the protocol and
interface names are removed and the interface version number is
reset.
</description>
<interface name="zwp_keyboard_shortcuts_inhibit_manager_v1" version="1">
<description summary="context object for keyboard grab_manager">
A global interface used for inhibiting the compositor keyboard shortcuts.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the keyboard shortcuts inhibitor object">
Destroy the keyboard shortcuts inhibitor manager.
</description>
</request>
<request name="inhibit_shortcuts">
<description summary="create a new keyboard shortcuts inhibitor object">
Create a new keyboard shortcuts inhibitor object associated with
the given surface for the given seat.
If shortcuts are already inhibited for the specified seat and surface,
a protocol error "already_inhibited" is raised by the compositor.
</description>
<arg name="id" type="new_id" interface="zwp_keyboard_shortcuts_inhibitor_v1"/>
<arg name="surface" type="object" interface="wl_surface"
summary="the surface that inhibits the keyboard shortcuts behavior"/>
<arg name="seat" type="object" interface="wl_seat"
summary="the wl_seat for which keyboard shortcuts should be disabled"/>
</request>
<enum name="error">
<entry name="already_inhibited"
value="0"
summary="the shortcuts are already inhibited for this surface"/>
</enum>
</interface>
<interface name="zwp_keyboard_shortcuts_inhibitor_v1" version="1">
<description summary="context object for keyboard shortcuts inhibitor">
A keyboard shortcuts inhibitor instructs the compositor to ignore
its own keyboard shortcuts when the associated surface has keyboard
focus. As a result, when the surface has keyboard focus on the given
seat, it will receive all key events originating from the specified
seat, even those which would normally be caught by the compositor for
its own shortcuts.
The Wayland compositor is however under no obligation to disable
all of its shortcuts, and may keep some special key combo for its own
use, including but not limited to one allowing the user to forcibly
restore normal keyboard events routing in the case of an unwilling
client. The compositor may also use the same key combo to reactivate
an existing shortcut inhibitor that was previously deactivated on
user request.
When the compositor restores its own keyboard shortcuts, an
"inactive" event is emitted to notify the client that the keyboard
shortcuts inhibitor is not effectively active for the surface and
seat any more, and the client should not expect to receive all
keyboard events.
When the keyboard shortcuts inhibitor is inactive, the client has
no way to forcibly reactivate the keyboard shortcuts inhibitor.
The user can chose to re-enable a previously deactivated keyboard
shortcuts inhibitor using any mechanism the compositor may offer,
in which case the compositor will send an "active" event to notify
the client.
If the surface is destroyed, unmapped, or loses the seat's keyboard
focus, the keyboard shortcuts inhibitor becomes irrelevant and the
compositor will restore its own keyboard shortcuts but no "inactive"
event is emitted in this case.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the keyboard shortcuts inhibitor object">
Remove the keyboard shortcuts inhibitor from the associated wl_surface.
</description>
</request>
<event name="active">
<description summary="shortcuts are inhibited">
This event indicates that the shortcut inhibitor is active.
The compositor sends this event every time compositor shortcuts
are inhibited on behalf of the surface. When active, the client
may receive input events normally reserved by the compositor
(see zwp_keyboard_shortcuts_inhibitor_v1).
This occurs typically when the initial request "inhibit_shortcuts"
first becomes active or when the user instructs the compositor to
re-enable and existing shortcuts inhibitor using any mechanism
offered by the compositor.
</description>
</event>
<event name="inactive">
<description summary="shortcuts are restored">
This event indicates that the shortcuts inhibitor is inactive,
normal shortcuts processing is restored by the compositor.
</description>
</event>
</interface>
</protocol>

View File

@@ -0,0 +1,407 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_layer_shell_unstable_v1">
<copyright>
Copyright © 2017 Drew DeVault
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="zwlr_layer_shell_v1" version="5">
<description summary="create surfaces that are layers of the desktop">
Clients can use this interface to assign the surface_layer role to
wl_surfaces. Such surfaces are assigned to a "layer" of the output and
rendered with a defined z-depth respective to each other. They may also be
anchored to the edges and corners of a screen and specify input handling
semantics. This interface should be suitable for the implementation of
many desktop shell components, and a broad number of other applications
that interact with the desktop.
</description>
<request name="get_layer_surface">
<description summary="create a layer_surface from a surface">
Create a layer surface for an existing surface. This assigns the role of
layer_surface, or raises a protocol error if another role is already
assigned.
Creating a layer surface from a wl_surface which has a buffer attached
or committed is a client error, and any attempts by a client to attach
or manipulate a buffer prior to the first layer_surface.configure call
must also be treated as errors.
After creating a layer_surface object and setting it up, the client
must perform an initial commit without any buffer attached.
The compositor will reply with a layer_surface.configure event.
The client must acknowledge it and is then allowed to attach a buffer
to map the surface.
You may pass NULL for output to allow the compositor to decide which
output to use. Generally this will be the one that the user most
recently interacted with.
Clients can specify a namespace that defines the purpose of the layer
surface.
</description>
<arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
<arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
<arg name="namespace" type="string" summary="namespace for the layer surface"/>
</request>
<enum name="error">
<entry name="role" value="0" summary="wl_surface has another role"/>
<entry name="invalid_layer" value="1" summary="layer value is invalid"/>
<entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
</enum>
<enum name="layer">
<description summary="available layers for surfaces">
These values indicate which layers a surface can be rendered in. They
are ordered by z depth, bottom-most first. Traditional shell surfaces
will typically be rendered between the bottom and top layers.
Fullscreen shell surfaces are typically rendered at the top layer.
Multiple surfaces can share a single layer, and ordering within a
single layer is undefined.
</description>
<entry name="background" value="0"/>
<entry name="bottom" value="1"/>
<entry name="top" value="2"/>
<entry name="overlay" value="3"/>
</enum>
<!-- Version 3 additions -->
<request name="destroy" type="destructor" since="3">
<description summary="destroy the layer_shell object">
This request indicates that the client will not use the layer_shell
object any more. Objects that have been created through this instance
are not affected.
</description>
</request>
</interface>
<interface name="zwlr_layer_surface_v1" version="5">
<description summary="layer metadata interface">
An interface that may be implemented by a wl_surface, for surfaces that
are designed to be rendered as a layer of a stacked desktop-like
environment.
Layer surface state (layer, size, anchor, exclusive zone,
margin, interactivity) is double-buffered, and will be applied at the
time wl_surface.commit of the corresponding wl_surface is called.
Attaching a null buffer to a layer surface unmaps it.
Unmapping a layer_surface means that the surface cannot be shown by the
compositor until it is explicitly mapped again. The layer_surface
returns to the state it had right after layer_shell.get_layer_surface.
The client can re-map the surface by performing a commit without any
buffer attached, waiting for a configure event and handling it as usual.
</description>
<request name="set_size">
<description summary="sets the size of the surface">
Sets the size of the surface in surface-local coordinates. The
compositor will display the surface centered with respect to its
anchors.
If you pass 0 for either value, the compositor will assign it and
inform you of the assignment in the configure event. You must set your
anchor to opposite edges in the dimensions you omit; not doing so is a
protocol error. Both values are 0 by default.
Size is double-buffered, see wl_surface.commit.
</description>
<arg name="width" type="uint"/>
<arg name="height" type="uint"/>
</request>
<request name="set_anchor">
<description summary="configures the anchor point of the surface">
Requests that the compositor anchor the surface to the specified edges
and corners. If two orthogonal edges are specified (e.g. 'top' and
'left'), then the anchor point will be the intersection of the edges
(e.g. the top left corner of the output); otherwise the anchor point
will be centered on that edge, or in the center if none is specified.
Anchor is double-buffered, see wl_surface.commit.
</description>
<arg name="anchor" type="uint" enum="anchor"/>
</request>
<request name="set_exclusive_zone">
<description summary="configures the exclusive geometry of this surface">
Requests that the compositor avoids occluding an area with other
surfaces. The compositor's use of this information is
implementation-dependent - do not assume that this region will not
actually be occluded.
A positive value is only meaningful if the surface is anchored to one
edge or an edge and both perpendicular edges. If the surface is not
anchored, anchored to only two perpendicular edges (a corner), anchored
to only two parallel edges or anchored to all edges, a positive value
will be treated the same as zero.
A positive zone is the distance from the edge in surface-local
coordinates to consider exclusive.
Surfaces that do not wish to have an exclusive zone may instead specify
how they should interact with surfaces that do. If set to zero, the
surface indicates that it would like to be moved to avoid occluding
surfaces with a positive exclusive zone. If set to -1, the surface
indicates that it would not like to be moved to accommodate for other
surfaces, and the compositor should extend it all the way to the edges
it is anchored to.
For example, a panel might set its exclusive zone to 10, so that
maximized shell surfaces are not shown on top of it. A notification
might set its exclusive zone to 0, so that it is moved to avoid
occluding the panel, but shell surfaces are shown underneath it. A
wallpaper or lock screen might set their exclusive zone to -1, so that
they stretch below or over the panel.
The default value is 0.
Exclusive zone is double-buffered, see wl_surface.commit.
</description>
<arg name="zone" type="int"/>
</request>
<request name="set_margin">
<description summary="sets a margin from the anchor point">
Requests that the surface be placed some distance away from the anchor
point on the output, in surface-local coordinates. Setting this value
for edges you are not anchored to has no effect.
The exclusive zone includes the margin.
Margin is double-buffered, see wl_surface.commit.
</description>
<arg name="top" type="int"/>
<arg name="right" type="int"/>
<arg name="bottom" type="int"/>
<arg name="left" type="int"/>
</request>
<enum name="keyboard_interactivity">
<description summary="types of keyboard interaction possible for a layer shell surface">
Types of keyboard interaction possible for layer shell surfaces. The
rationale for this is twofold: (1) some applications are not interested
in keyboard events and not allowing them to be focused can improve the
desktop experience; (2) some applications will want to take exclusive
keyboard focus.
</description>
<entry name="none" value="0">
<description summary="no keyboard focus is possible">
This value indicates that this surface is not interested in keyboard
events and the compositor should never assign it the keyboard focus.
This is the default value, set for newly created layer shell surfaces.
This is useful for e.g. desktop widgets that display information or
only have interaction with non-keyboard input devices.
</description>
</entry>
<entry name="exclusive" value="1">
<description summary="request exclusive keyboard focus">
Request exclusive keyboard focus if this surface is above the shell surface layer.
For the top and overlay layers, the seat will always give
exclusive keyboard focus to the top-most layer which has keyboard
interactivity set to exclusive. If this layer contains multiple
surfaces with keyboard interactivity set to exclusive, the compositor
determines the one receiving keyboard events in an implementation-
defined manner. In this case, no guarantee is made when this surface
will receive keyboard focus (if ever).
For the bottom and background layers, the compositor is allowed to use
normal focus semantics.
This setting is mainly intended for applications that need to ensure
they receive all keyboard events, such as a lock screen or a password
prompt.
</description>
</entry>
<entry name="on_demand" value="2" since="4">
<description summary="request regular keyboard focus semantics">
This requests the compositor to allow this surface to be focused and
unfocused by the user in an implementation-defined manner. The user
should be able to unfocus this surface even regardless of the layer
it is on.
Typically, the compositor will want to use its normal mechanism to
manage keyboard focus between layer shell surfaces with this setting
and regular toplevels on the desktop layer (e.g. click to focus).
Nevertheless, it is possible for a compositor to require a special
interaction to focus or unfocus layer shell surfaces (e.g. requiring
a click even if focus follows the mouse normally, or providing a
keybinding to switch focus between layers).
This setting is mainly intended for desktop shell components (e.g.
panels) that allow keyboard interaction. Using this option can allow
implementing a desktop shell that can be fully usable without the
mouse.
</description>
</entry>
</enum>
<request name="set_keyboard_interactivity">
<description summary="requests keyboard events">
Set how keyboard events are delivered to this surface. By default,
layer shell surfaces do not receive keyboard events; this request can
be used to change this.
This setting is inherited by child surfaces set by the get_popup
request.
Layer surfaces receive pointer, touch, and tablet events normally. If
you do not want to receive them, set the input region on your surface
to an empty region.
Keyboard interactivity is double-buffered, see wl_surface.commit.
</description>
<arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
</request>
<request name="get_popup">
<description summary="assign this layer_surface as an xdg_popup parent">
This assigns an xdg_popup's parent to this layer_surface. This popup
should have been created via xdg_surface::get_popup with the parent set
to NULL, and this request must be invoked before committing the popup's
initial state.
See the documentation of xdg_popup for more details about what an
xdg_popup is and how it is used.
</description>
<arg name="popup" type="object" interface="xdg_popup"/>
</request>
<request name="ack_configure">
<description summary="ack a configure event">
When a configure event is received, if a client commits the
surface in response to the configure event, then the client
must make an ack_configure request sometime before the commit
request, passing along the serial of the configure event.
If the client receives multiple configure events before it
can respond to one, it only has to ack the last configure event.
A client is not required to commit immediately after sending
an ack_configure request - it may even ack_configure several times
before its next surface commit.
A client may send multiple ack_configure requests before committing, but
only the last request sent before a commit indicates which configure
event the client really is responding to.
</description>
<arg name="serial" type="uint" summary="the serial from the configure event"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the layer_surface">
This request destroys the layer surface.
</description>
</request>
<event name="configure">
<description summary="suggest a surface change">
The configure event asks the client to resize its surface.
Clients should arrange their surface for the new states, and then send
an ack_configure request with the serial sent in this configure event at
some point before committing the new surface.
The client is free to dismiss all but the last configure event it
received.
The width and height arguments specify the size of the window in
surface-local coordinates.
The size is a hint, in the sense that the client is free to ignore it if
it doesn't resize, pick a smaller size (to satisfy aspect ratio or
resize in steps of NxM pixels). If the client picks a smaller size and
is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
surface will be centered on this axis.
If the width or height arguments are zero, it means the client should
decide its own window dimension.
</description>
<arg name="serial" type="uint"/>
<arg name="width" type="uint"/>
<arg name="height" type="uint"/>
</event>
<event name="closed">
<description summary="surface should be closed">
The closed event is sent by the compositor when the surface will no
longer be shown. The output may have been destroyed or the user may
have asked for it to be removed. Further changes to the surface will be
ignored. The client should destroy the resource after receiving this
event, and create a new surface if they so choose.
</description>
</event>
<enum name="error">
<entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
<entry name="invalid_size" value="1" summary="size is invalid"/>
<entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
<entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
<entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
</enum>
<enum name="anchor" bitfield="true">
<entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
<entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
<entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
<entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
</enum>
<!-- Version 2 additions -->
<request name="set_layer" since="2">
<description summary="change the layer of the surface">
Change the layer that the surface is rendered on.
Layer is double-buffered, see wl_surface.commit.
</description>
<arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
</request>
<!-- Version 5 additions -->
<request name="set_exclusive_edge" since="5">
<description summary="set the edge the exclusive zone will be applied to">
Requests an edge for the exclusive zone to apply. The exclusive
edge will be automatically deduced from anchor points when possible,
but when the surface is anchored to a corner, it will be necessary
to set it explicitly to disambiguate, as it is not possible to deduce
which one of the two corner edges should be used.
The edge must be one the surface is anchored to, otherwise the
invalid_exclusive_edge protocol error will be raised.
</description>
<arg name="edge" type="uint" enum="anchor"/>
</request>
</interface>
</protocol>

View File

@@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_screencopy_unstable_v1">
<copyright>
Copyright © 2018 Simon Ser
Copyright © 2019 Andri Yngvason
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="screen content capturing on client buffers">
This protocol allows clients to ask the compositor to copy part of the
screen content to a client buffer.
Warning! The protocol described in this file is experimental and
backward incompatible changes may be made. Backward compatible changes
may be added together with the corresponding interface version bump.
Backward incompatible changes are done by bumping the version number in
the protocol and interface names and resetting the interface version.
Once the protocol is to be declared stable, the 'z' prefix and the
version number in the protocol and interface names are removed and the
interface version number is reset.
Note! This protocol is deprecated and not intended for production use.
The ext-image-copy-capture-v1 protocol should be used instead.
</description>
<interface name="zwlr_screencopy_manager_v1" version="3">
<description summary="manager to inform clients and begin capturing">
This object is a manager which offers requests to start capturing from a
source.
</description>
<request name="capture_output">
<description summary="capture an output">
Capture the next frame of an entire output.
</description>
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="output" type="object" interface="wl_output"/>
</request>
<request name="capture_output_region">
<description summary="capture an output's region">
Capture the next frame of an output's region.
The region is given in output logical coordinates, see
xdg_output.logical_size. The region will be clipped to the output's
extents.
</description>
<arg name="frame" type="new_id" interface="zwlr_screencopy_frame_v1"/>
<arg name="overlay_cursor" type="int"
summary="composite cursor onto the frame"/>
<arg name="output" type="object" interface="wl_output"/>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
All objects created by the manager will still remain valid, until their
appropriate destroy request has been called.
</description>
</request>
</interface>
<interface name="zwlr_screencopy_frame_v1" version="3">
<description summary="a frame ready for copy">
This object represents a single frame.
When created, a series of buffer events will be sent, each representing a
supported buffer type. The "buffer_done" event is sent afterwards to
indicate that all supported buffer types have been enumerated. The client
will then be able to send a "copy" request. If the capture is successful,
the compositor will send a "flags" event followed by a "ready" event.
For objects version 2 or lower, wl_shm buffers are always supported, ie.
the "buffer" event is guaranteed to be sent.
If the capture failed, the "failed" event is sent. This can happen anytime
before the "ready" event.
Once either a "ready" or a "failed" event is received, the client should
destroy the frame.
</description>
<event name="buffer">
<description summary="wl_shm buffer information">
Provides information about wl_shm buffer parameters that need to be
used for this frame. This event is sent once after the frame is created
if wl_shm buffers are supported.
</description>
<arg name="format" type="uint" enum="wl_shm.format" summary="buffer format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
<arg name="stride" type="uint" summary="buffer stride"/>
</event>
<request name="copy">
<description summary="copy the frame">
Copy the frame to the supplied buffer. The buffer must have the
correct size, see zwlr_screencopy_frame_v1.buffer and
zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a
supported format.
If the frame is successfully copied, "flags" and "ready" events are
sent. Otherwise, a "failed" event is sent.
</description>
<arg name="buffer" type="object" interface="wl_buffer"/>
</request>
<enum name="error">
<entry name="already_used" value="0"
summary="the object has already been used to copy a wl_buffer"/>
<entry name="invalid_buffer" value="1"
summary="buffer attributes are invalid"/>
</enum>
<enum name="flags" bitfield="true">
<entry name="y_invert" value="1" summary="contents are y-inverted"/>
</enum>
<event name="flags">
<description summary="frame flags">
Provides flags about the frame. This event is sent once before the
"ready" event.
</description>
<arg name="flags" type="uint" enum="flags" summary="frame flags"/>
</event>
<event name="ready">
<description summary="indicates frame is available for reading">
Called as soon as the frame is copied, indicating it is available
for reading. This event includes the time at which the presentation took place.
The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
each component being an unsigned 32-bit value. Whole seconds are in
tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
and the additional fractional part in tv_nsec as nanoseconds. Hence,
for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
may have an arbitrary offset at start.
After receiving this event, the client should destroy the object.
</description>
<arg name="tv_sec_hi" type="uint"
summary="high 32 bits of the seconds part of the timestamp"/>
<arg name="tv_sec_lo" type="uint"
summary="low 32 bits of the seconds part of the timestamp"/>
<arg name="tv_nsec" type="uint"
summary="nanoseconds part of the timestamp"/>
</event>
<event name="failed">
<description summary="frame copy failed">
This event indicates that the attempted frame copy has failed.
After receiving this event, the client should destroy the object.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="delete this object, used or not">
Destroys the frame. This request can be sent at any time by the client.
</description>
</request>
<!-- Version 2 additions -->
<request name="copy_with_damage" since="2">
<description summary="copy the frame when it's damaged">
Same as copy, except it waits until there is damage to copy.
</description>
<arg name="buffer" type="object" interface="wl_buffer"/>
</request>
<event name="damage" since="2">
<description summary="carries the coordinates of the damaged region">
This event is sent right before the ready event when copy_with_damage is
requested. It may be generated multiple times for each copy_with_damage
request.
The arguments describe a box around an area that has changed since the
last copy request that was derived from the current screencopy manager
instance.
The union of all regions received between the call to copy_with_damage
and a ready event is the total damage since the prior ready event.
</description>
<arg name="x" type="uint" summary="damaged x coordinates"/>
<arg name="y" type="uint" summary="damaged y coordinates"/>
<arg name="width" type="uint" summary="current width"/>
<arg name="height" type="uint" summary="current height"/>
</event>
<!-- Version 3 additions -->
<event name="linux_dmabuf" since="3">
<description summary="linux-dmabuf buffer information">
Provides information about linux-dmabuf buffer parameters that need to
be used for this frame. This event is sent once after the frame is
created if linux-dmabuf buffers are supported.
</description>
<arg name="format" type="uint" summary="fourcc pixel format"/>
<arg name="width" type="uint" summary="buffer width"/>
<arg name="height" type="uint" summary="buffer height"/>
</event>
<event name="buffer_done" since="3">
<description summary="all buffer types reported">
This event is sent once after all buffer events have been sent.
The client should proceed to create a buffer of one of the supported
types, and send a "copy" request.
</description>
</event>
</interface>
</protocol>

View File

@@ -0,0 +1,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")
}

View File

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

View File

@@ -0,0 +1,180 @@
package screenshot
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
)
const (
notifyDest = "org.freedesktop.Notifications"
notifyPath = "/org/freedesktop/Notifications"
notifyInterface = "org.freedesktop.Notifications"
)
type NotifyResult struct {
FilePath string
Clipboard bool
ImageData []byte
Width int
Height int
}
func SendNotification(result NotifyResult) {
conn, err := dbus.SessionBus()
if err != nil {
log.Debug("dbus session failed", "err", err)
return
}
var actions []string
if result.FilePath != "" {
actions = []string{"default", "Open"}
}
hints := map[string]dbus.Variant{}
if len(result.ImageData) > 0 && result.Width > 0 && result.Height > 0 {
rowstride := result.Width * 3
hints["image_data"] = dbus.MakeVariant(struct {
Width int32
Height int32
Rowstride int32
HasAlpha bool
BitsPerSample int32
Channels int32
Data []byte
}{
Width: int32(result.Width),
Height: int32(result.Height),
Rowstride: int32(rowstride),
HasAlpha: false,
BitsPerSample: 8,
Channels: 3,
Data: result.ImageData,
})
} else if result.FilePath != "" {
hints["image_path"] = dbus.MakeVariant(result.FilePath)
}
summary := "Screenshot captured"
body := ""
if result.Clipboard && result.FilePath != "" {
body = fmt.Sprintf("Copied to clipboard\n%s", filepath.Base(result.FilePath))
} else if result.Clipboard {
body = "Copied to clipboard"
} else if result.FilePath != "" {
body = filepath.Base(result.FilePath)
}
obj := conn.Object(notifyDest, notifyPath)
call := obj.Call(
notifyInterface+".Notify",
0,
"DMS",
uint32(0),
"",
summary,
body,
actions,
hints,
int32(5000),
)
if call.Err != nil {
log.Debug("notify call failed", "err", call.Err)
return
}
var notificationID uint32
if err := call.Store(&notificationID); err != nil {
log.Debug("failed to get notification id", "err", err)
return
}
if len(actions) == 0 || result.FilePath == "" {
return
}
spawnActionListener(notificationID, result.FilePath)
}
func spawnActionListener(notificationID uint32, filePath string) {
exe, err := os.Executable()
if err != nil {
log.Debug("failed to get executable", "err", err)
return
}
cmd := exec.Command(exe, "notify-action", fmt.Sprintf("%d", notificationID), filePath)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
cmd.Start()
}
func RunNotifyActionListener(args []string) {
if len(args) < 2 {
return
}
notificationID, err := strconv.ParseUint(args[0], 10, 32)
if err != nil {
return
}
filePath := args[1]
conn, err := dbus.SessionBus()
if err != nil {
return
}
if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(notifyPath),
dbus.WithMatchInterface(notifyInterface),
); err != nil {
return
}
signals := make(chan *dbus.Signal, 10)
conn.Signal(signals)
for sig := range signals {
switch sig.Name {
case notifyInterface + ".ActionInvoked":
if len(sig.Body) < 2 {
continue
}
id, ok := sig.Body[0].(uint32)
if !ok || id != uint32(notificationID) {
continue
}
openFile(filePath)
return
case notifyInterface + ".NotificationClosed":
if len(sig.Body) < 1 {
continue
}
id, ok := sig.Body[0].(uint32)
if !ok || id != uint32(notificationID) {
continue
}
return
}
}
}
func openFile(filePath string) {
cmd := exec.Command("xdg-open", filePath)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
cmd.Start()
}

View File

@@ -0,0 +1,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()
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,6 +40,10 @@ func (b *DDCBackend) scanI2CDevices() error {
return b.scanI2CDevicesInternal(false) 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)

View File

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

View File

@@ -5,6 +5,8 @@ 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"
@@ -12,6 +14,9 @@ import (
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()
} }
} }

View File

@@ -0,0 +1,91 @@
package server
import (
"context"
"net"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type MatugenQueueResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
}
func handleMatugenQueue(conn net.Conn, req models.Request) {
getString := func(key string) string {
if v, ok := req.Params[key].(string); ok {
return v
}
return ""
}
getBool := func(key string, def bool) bool {
if v, ok := req.Params[key].(bool); ok {
return v
}
return def
}
opts := matugen.Options{
StateDir: getString("stateDir"),
ShellDir: getString("shellDir"),
ConfigDir: getString("configDir"),
Kind: getString("kind"),
Value: getString("value"),
Mode: getString("mode"),
IconTheme: getString("iconTheme"),
MatugenType: getString("matugenType"),
RunUserTemplates: getBool("runUserTemplates", true),
StockColors: getString("stockColors"),
SyncModeWithPortal: getBool("syncModeWithPortal", false),
TerminalsAlwaysDark: getBool("terminalsAlwaysDark", false),
}
wait := getBool("wait", true)
queue := matugen.GetQueue()
resultCh := queue.Submit(opts)
if !wait {
models.Respond(conn, req.ID, MatugenQueueResult{
Success: true,
Message: "queued",
})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
select {
case result := <-resultCh:
if result.Error != nil {
if result.Error == context.Canceled {
models.Respond(conn, req.ID, MatugenQueueResult{
Success: false,
Message: "cancelled",
})
return
}
models.RespondError(conn, req.ID, result.Error.Error())
return
}
models.Respond(conn, req.ID, MatugenQueueResult{
Success: true,
Message: "completed",
})
case <-ctx.Done():
models.RespondError(conn, req.ID, "timeout waiting for theme generation")
}
}
func handleMatugenStatus(conn net.Conn, req models.Request) {
queue := matugen.GetQueue()
models.Respond(conn, req.ID, map[string]bool{
"running": queue.IsRunning(),
"pending": queue.HasPending(),
})
}

View File

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

View File

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

View File

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

View File

@@ -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,16 +224,17 @@ 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.Context().DeleteID(e.Id)
if i.deleteIdHandler != nil {
i.deleteIdHandler(e) i.deleteIdHandler(e)
} }
}
} }
// RegistryInterfaceName is the name of the interface as it appears in the [client.Registry]. // RegistryInterfaceName is the name of the interface as it appears in the [client.Registry].
@@ -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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,6 @@ Requires: dgop
Recommends: cava Recommends: cava
Recommends: cliphist Recommends: cliphist
Recommends: danksearch Recommends: danksearch
Recommends: hyprpicker
Recommends: matugen Recommends: matugen
Recommends: quickshell-git Recommends: quickshell-git
Recommends: wl-clipboard Recommends: wl-clipboard

View File

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

View File

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

View File

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

View File

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

View File

@@ -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).";
};
}; };
}; };
} }

View File

@@ -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]}"
if [[ "$IS_MANUAL" == true ]]; then
NEXT_RELEASE=$((BASE_RELEASE + 1)) NEXT_RELEASE=$((BASE_RELEASE + 1))
echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)" 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" 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,7 +668,8 @@ 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
if [[ "$IS_MANUAL" == true ]]; then
echo "==> Detected rebuild of same base version $CHANGELOG_BASE, incrementing version" 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
@@ -800,18 +826,24 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[
rm -rf "$EXTRACT_DIR" rm -rf "$EXTRACT_DIR"
exit 1 exit 1
fi fi
else
echo " Error: No existing tarball found to extract"
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")" cd "$(dirname "$SOURCE_DIR")"
TARBALL_BASE=$(basename "$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" tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
cd "$WORK_DIR" cd "$REPO_ROOT"
fi
else
echo "==> Detected same version. Not a manual run, skipping Debian version increment."
echo "✅ No changes needed for Debian. Exiting."
exit 0
fi
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null) TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1) TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
@@ -852,10 +884,7 @@ Files:
$TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL $TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL
EOF EOF
echo " - Updated changelog and recreated tarball with version $NEW_VERSION" echo " - Updated changelog and recreated tarball with version $NEW_VERSION"
else
echo " Error: Source directory not found, cannot recreate tarball"
exit 1
fi
fi fi
fi fi

View File

@@ -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))
if [[ "$IS_MANUAL" == true ]]; then
info "Detected rebuild of same commit (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM" 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))
if [[ "$IS_MANUAL" == true ]]; then
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM" 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

View File

@@ -84,8 +84,9 @@ fi
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1) CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
if [ -z "$CHANGES_FILE" ]; then if [ -z "$CHANGES_FILE" ]; then
error "Changes file not found in $PARENT_DIR" warn "Changes file not found in $PARENT_DIR"
exit 1 warn "Assuming build was skipped (no changes needed) and exiting successfully."
exit 0
fi fi
info "Found changes file: $CHANGES_FILE" info "Found changes file: $CHANGES_FILE"

36
flake.lock generated
View File

@@ -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"
} }
} }
}, },

118
flake.nix
View File

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

View File

@@ -18,6 +18,7 @@ const KEY_MAP = {
96: "grave", 96: "grave",
32: "space", 32: "space",
16777225: "Print", 16777225: "Print",
16777226: "Print",
16777220: "Return", 16777220: "Return",
16777221: "Return", 16777221: "Return",
16777217: "Tab", 16777217: "Tab",
@@ -93,20 +94,20 @@ function xkbKeyFromQtKey(qk) {
function modsFromEvent(mods) { function modsFromEvent(mods) {
var result = []; var result = [];
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)
result.push("Shift");
var hasAlt = mods & 0x08000000; var hasAlt = mods & 0x08000000;
var hasSuper = mods & 0x10000000; var hasSuper = mods & 0x10000000;
if (hasAlt && hasSuper) { if (hasAlt && hasSuper) {
result.push("Mod"); result.push("Mod");
} else { } else {
if (hasAlt)
result.push("Alt");
if (hasSuper) if (hasSuper)
result.push("Super"); result.push("Super");
if (hasAlt)
result.push("Alt");
} }
if (mods & 0x04000000)
result.push("Ctrl");
if (mods & 0x02000000)
result.push("Shift");
return result; return result;
} }

View File

@@ -34,7 +34,7 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" }, { id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" }, { id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" }, { id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
{ id: "spawn dms ipc call dash toggle", label: "Dashboard: Toggle" }, { id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" }, { id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" }, { id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
{ id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" }, { id: "spawn dms ipc call dash open weather", label: "Dashboard: Weather" },
@@ -59,6 +59,7 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" }, { id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" },
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" }, { id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" }, { id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" },
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
{ id: "spawn dms ipc call brightness increment", label: "Brightness Up" }, { id: "spawn dms ipc call brightness increment", label: "Brightness Up" },
{ id: "spawn dms ipc call brightness increment 1", label: "Brightness Up (1%)" }, { id: "spawn dms ipc call brightness increment 1", label: "Brightness Up (1%)" },
{ id: "spawn dms ipc call brightness increment 5", label: "Brightness Up (5%)" }, { id: "spawn dms ipc call brightness increment 5", label: "Brightness Up (5%)" },
@@ -108,9 +109,15 @@ const COMPOSITOR_ACTIONS = {
{ id: "fullscreen-window", label: "Fullscreen" }, { id: "fullscreen-window", label: "Fullscreen" },
{ id: "maximize-column", label: "Maximize Column" }, { id: "maximize-column", label: "Maximize Column" },
{ id: "center-column", label: "Center Column" }, { id: "center-column", label: "Center Column" },
{ id: "center-visible-columns", label: "Center Visible Columns" },
{ id: "toggle-window-floating", label: "Toggle Floating" }, { id: "toggle-window-floating", label: "Toggle Floating" },
{ id: "switch-focus-between-floating-and-tiling", label: "Switch Floating/Tiling Focus" },
{ id: "switch-preset-column-width", label: "Cycle Column Width" }, { id: "switch-preset-column-width", label: "Cycle Column Width" },
{ id: "switch-preset-window-height", label: "Cycle Window Height" }, { id: "switch-preset-window-height", label: "Cycle Window Height" },
{ id: "set-column-width", label: "Set Column Width" },
{ id: "set-window-height", label: "Set Window Height" },
{ id: "reset-window-height", label: "Reset Window Height" },
{ id: "expand-column-to-available-width", label: "Expand to Available Width" },
{ id: "consume-or-expel-window-left", label: "Consume/Expel Left" }, { id: "consume-or-expel-window-left", label: "Consume/Expel Left" },
{ id: "consume-or-expel-window-right", label: "Consume/Expel Right" }, { id: "consume-or-expel-window-right", label: "Consume/Expel Right" },
{ id: "toggle-column-tabbed-display", label: "Toggle Tabbed" } { id: "toggle-column-tabbed-display", label: "Toggle Tabbed" }
@@ -135,8 +142,10 @@ const COMPOSITOR_ACTIONS = {
{ id: "focus-workspace-down", label: "Focus Workspace Down" }, { id: "focus-workspace-down", label: "Focus Workspace Down" },
{ id: "focus-workspace-up", label: "Focus Workspace Up" }, { id: "focus-workspace-up", label: "Focus Workspace Up" },
{ id: "focus-workspace-previous", label: "Focus Previous Workspace" }, { id: "focus-workspace-previous", label: "Focus Previous Workspace" },
{ id: "focus-workspace", label: "Focus Workspace (by index)" },
{ id: "move-column-to-workspace-down", label: "Move to Workspace Down" }, { id: "move-column-to-workspace-down", label: "Move to Workspace Down" },
{ id: "move-column-to-workspace-up", label: "Move to Workspace Up" }, { id: "move-column-to-workspace-up", label: "Move to Workspace Up" },
{ id: "move-column-to-workspace", label: "Move to Workspace (by index)" },
{ id: "move-workspace-down", label: "Move Workspace Down" }, { id: "move-workspace-down", label: "Move Workspace Down" },
{ id: "move-workspace-up", label: "Move Workspace Up" } { id: "move-workspace-up", label: "Move Workspace Up" }
], ],
@@ -172,6 +181,64 @@ const COMPOSITOR_ACTIONS = {
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"]; const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"];
const ACTION_ARGS = {
"set-column-width": {
args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }]
},
"set-window-height": {
args: [{ name: "value", type: "text", label: "Height", placeholder: "+10%, -10%, 50%" }]
},
"focus-workspace": {
args: [{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." }]
},
"move-column-to-workspace": {
args: [
{ name: "index", type: "number", label: "Workspace", placeholder: "1, 2, 3..." },
{ name: "focus", type: "bool", label: "Follow focus", default: false }
]
},
"move-column-to-workspace-down": {
args: [{ name: "focus", type: "bool", label: "Follow focus", default: false }]
},
"move-column-to-workspace-up": {
args: [{ name: "focus", type: "bool", label: "Follow focus", default: false }]
},
"screenshot": {
args: [{ name: "show-pointer", type: "bool", label: "Show pointer" }]
},
"screenshot-screen": {
args: [
{ name: "show-pointer", type: "bool", label: "Show pointer" },
{ name: "write-to-disk", type: "bool", label: "Save to disk" }
]
},
"screenshot-window": {
args: [
{ name: "show-pointer", type: "bool", label: "Show pointer" },
{ name: "write-to-disk", type: "bool", label: "Save to disk" }
]
}
};
const DMS_ACTION_ARGS = {
"audio increment": {
base: "spawn dms ipc call audio increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"audio decrement": {
base: "spawn dms ipc call audio decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness increment": {
base: "spawn dms ipc call brightness increment",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
},
"brightness decrement": {
base: "spawn dms ipc call brightness decrement",
args: [{ name: "amount", type: "number", label: "Amount %", placeholder: "5", default: "" }]
}
};
function getActionTypes() { function getActionTypes() {
return ACTION_TYPES; return ACTION_TYPES;
} }
@@ -233,11 +300,12 @@ function getActionLabel(action) {
if (!action) if (!action)
return ""; return "";
const dmsAct = findDmsAction(action); var dmsAct = findDmsAction(action);
if (dmsAct) if (dmsAct)
return dmsAct.label; return dmsAct.label;
const compAct = findCompositorAction(action); var base = action.split(" ")[0];
var compAct = findCompositorAction(base);
if (compAct) if (compAct)
return compAct.label; return compAct.label;
@@ -282,7 +350,8 @@ function isValidAction(action) {
function isKnownCompositorAction(action) { function isKnownCompositorAction(action) {
if (!action) if (!action)
return false; return false;
return findCompositorAction(action) !== null; var base = action.split(" ")[0];
return findCompositorAction(base) !== null;
} }
function buildSpawnAction(command, args) { function buildSpawnAction(command, args) {
@@ -321,3 +390,135 @@ function parseShellCommand(action) {
content = content.slice(1, -1); content = content.slice(1, -1);
return content.replace(/\\"/g, "\""); return content.replace(/\\"/g, "\"");
} }
function getActionArgConfig(action) {
if (!action)
return null;
var baseAction = action.split(" ")[0];
if (ACTION_ARGS[baseAction])
return { type: "compositor", base: baseAction, config: ACTION_ARGS[baseAction] };
for (var key in DMS_ACTION_ARGS) {
if (action.startsWith(DMS_ACTION_ARGS[key].base))
return { type: "dms", base: key, config: DMS_ACTION_ARGS[key] };
}
return null;
}
function parseCompositorActionArgs(action) {
if (!action)
return { base: "", args: {} };
var parts = action.split(" ");
var base = parts[0];
var args = {};
if (!ACTION_ARGS[base])
return { base: action, args: {} };
var argParts = parts.slice(1);
switch (base) {
case "move-column-to-workspace":
for (var i = 0; i < argParts.length; i++) {
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
args.focus = argParts[i] === "focus=true";
} else if (!args.index) {
args.index = argParts[i];
}
}
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
for (var k = 0; k < argParts.length; k++) {
if (argParts[k] === "focus=true" || argParts[k] === "focus=false")
args.focus = argParts[k] === "focus=true";
}
break;
default:
if (base.startsWith("screenshot")) {
for (var j = 0; j < argParts.length; j++) {
var kv = argParts[j].split("=");
if (kv.length === 2)
args[kv[0]] = kv[1] === "true";
}
} else if (argParts.length > 0) {
args.value = argParts.join(" ");
}
}
return { base: base, args: args };
}
function buildCompositorAction(base, args) {
if (!base)
return "";
var parts = [base];
if (!args || Object.keys(args).length === 0)
return base;
switch (base) {
case "move-column-to-workspace":
if (args.index)
parts.push(args.index);
if (args.focus === false)
parts.push("focus=false");
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
if (args.focus === false)
parts.push("focus=false");
break;
default:
if (base.startsWith("screenshot")) {
if (args["show-pointer"] === true)
parts.push("show-pointer=true");
if (args["write-to-disk"] === true)
parts.push("write-to-disk=true");
} else if (args.value) {
parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
}
}
return parts.join(" ");
}
function parseDmsActionArgs(action) {
if (!action)
return { base: "", args: {} };
for (var key in DMS_ACTION_ARGS) {
var config = DMS_ACTION_ARGS[key];
if (action.startsWith(config.base)) {
var rest = action.slice(config.base.length).trim();
return { base: key, args: { amount: rest || "" } };
}
}
return { base: action, args: {} };
}
function buildDmsAction(baseKey, args) {
var config = DMS_ACTION_ARGS[baseKey];
if (!config)
return "";
var action = config.base;
if (args && args.amount)
action += " " + args.amount;
return action;
}
function getScreenshotOptions() {
return [
{ id: "write-to-disk", label: "Save to disk", type: "bool" },
{ id: "show-pointer", label: "Show pointer", type: "bool" }
];
}

View File

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

View File

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

View File

@@ -105,7 +105,7 @@ Singleton {
property bool controlCenterShowNetworkIcon: true property bool controlCenterShowNetworkIcon: true
property bool controlCenterShowBluetoothIcon: true property bool controlCenterShowBluetoothIcon: true
property bool controlCenterShowAudioIcon: true property bool controlCenterShowAudioIcon: true
property bool controlCenterShowVpnIcon: false property bool controlCenterShowVpnIcon: true
property bool controlCenterShowBrightnessIcon: false property bool controlCenterShowBrightnessIcon: false
property bool controlCenterShowMicIcon: false property bool controlCenterShowMicIcon: false
property bool controlCenterShowBatteryIcon: false property bool controlCenterShowBatteryIcon: false
@@ -248,10 +248,12 @@ Singleton {
property int acLockTimeout: 0 property int acLockTimeout: 0
property int acSuspendTimeout: 0 property int acSuspendTimeout: 0
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
property string acProfileName: ""
property int batteryMonitorTimeout: 0 property int batteryMonitorTimeout: 0
property int batteryLockTimeout: 0 property int batteryLockTimeout: 0
property int batterySuspendTimeout: 0 property int batterySuspendTimeout: 0
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
property string batteryProfileName: ""
property bool lockBeforeSuspend: false property bool lockBeforeSuspend: false
property bool preventIdleForMedia: false property bool preventIdleForMedia: false
property bool loginctlLockIntegration: true property bool loginctlLockIntegration: true
@@ -293,8 +295,10 @@ Singleton {
property bool lockScreenShowPowerActions: true property bool lockScreenShowPowerActions: true
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 3 property int maxFprintTries: 15
property bool fprintdAvailable: false property bool fprintdAvailable: false
property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000"
property bool hideBrightnessSlider: false property bool hideBrightnessSlider: false
property int notificationTimeoutLow: 5000 property int notificationTimeoutLow: 5000
@@ -311,9 +315,10 @@ Singleton {
property bool osdMicMuteEnabled: true property bool osdMicMuteEnabled: true
property bool osdCapsLockEnabled: true property bool osdCapsLockEnabled: true
property bool osdPowerProfileEnabled: true property bool osdPowerProfileEnabled: true
property bool osdAudioOutputEnabled: true
property bool powerActionConfirm: true property bool powerActionConfirm: true
property int powerActionHoldDuration: 1 property real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
property string powerMenuDefaultAction: "logout" property string powerMenuDefaultAction: "logout"
property bool powerMenuGridLayout: false property bool powerMenuGridLayout: false
@@ -367,7 +372,8 @@ Singleton {
openOnOverview: false, openOnOverview: false,
visible: true, visible: true,
popupGapsAuto: true, popupGapsAuto: true,
popupGapsManual: 4 popupGapsManual: 4,
maximizeDetection: true
} }
] ]

View File

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

View File

@@ -51,7 +51,7 @@ var SPEC = {
controlCenterShowNetworkIcon: { def: true }, controlCenterShowNetworkIcon: { def: true },
controlCenterShowBluetoothIcon: { def: true }, controlCenterShowBluetoothIcon: { def: true },
controlCenterShowAudioIcon: { def: true }, controlCenterShowAudioIcon: { def: true },
controlCenterShowVpnIcon: { def: false }, controlCenterShowVpnIcon: { def: true },
controlCenterShowBrightnessIcon: { def: false }, controlCenterShowBrightnessIcon: { def: false },
controlCenterShowMicIcon: { def: false }, controlCenterShowMicIcon: { def: false },
controlCenterShowBatteryIcon: { def: false }, controlCenterShowBatteryIcon: { def: false },
@@ -147,10 +147,12 @@ var SPEC = {
acLockTimeout: { def: 0 }, acLockTimeout: { def: 0 },
acSuspendTimeout: { def: 0 }, acSuspendTimeout: { def: 0 },
acSuspendBehavior: { def: 0 }, acSuspendBehavior: { def: 0 },
acProfileName: { def: "" },
batteryMonitorTimeout: { def: 0 }, batteryMonitorTimeout: { def: 0 },
batteryLockTimeout: { def: 0 }, batteryLockTimeout: { def: 0 },
batterySuspendTimeout: { def: 0 }, batterySuspendTimeout: { def: 0 },
batterySuspendBehavior: { def: 0 }, batterySuspendBehavior: { def: 0 },
batteryProfileName: { def: "" },
lockBeforeSuspend: { def: false }, lockBeforeSuspend: { def: false },
preventIdleForMedia: { def: false }, preventIdleForMedia: { def: false },
loginctlLockIntegration: { def: true }, loginctlLockIntegration: { def: true },
@@ -192,8 +194,10 @@ var SPEC = {
lockScreenShowPowerActions: { def: true }, lockScreenShowPowerActions: { def: true },
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 3 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" },
hideBrightnessSlider: { def: false }, hideBrightnessSlider: { def: false },
notificationTimeoutLow: { def: 5000 }, notificationTimeoutLow: { def: 5000 },
@@ -210,9 +214,10 @@ var SPEC = {
osdMicMuteEnabled: { def: true }, osdMicMuteEnabled: { def: true },
osdCapsLockEnabled: { def: true }, osdCapsLockEnabled: { def: true },
osdPowerProfileEnabled: { def: false }, osdPowerProfileEnabled: { def: false },
osdAudioOutputEnabled: { def: true },
powerActionConfirm: { def: true }, powerActionConfirm: { def: true },
powerActionHoldDuration: { def: 1 }, powerActionHoldDuration: { def: 0.5 },
powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] }, powerMenuActions: { def: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] },
powerMenuDefaultAction: { def: "logout" }, powerMenuDefaultAction: { def: "logout" },
powerMenuGridLayout: { def: false }, powerMenuGridLayout: { def: false },
@@ -265,7 +270,8 @@ var SPEC = {
openOnOverview: false, openOnOverview: false,
visible: true, visible: true,
popupGapsAuto: true, popupGapsAuto: true,
popupGapsManual: 4 popupGapsManual: 4,
maximizeDetection: true
}], onChange: "updateBarConfigs" } }], onChange: "updateBarConfigs" }
}; };

View File

@@ -700,6 +700,14 @@ Item {
} }
} }
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: AudioOutputOSD {
modelData: item
}
}
LazyLoader { LazyLoader {
id: hyprlandOverviewLoader id: hyprlandOverviewLoader
active: CompositorService.isHyprland active: CompositorService.isHyprland

View File

@@ -648,6 +648,13 @@ Item {
return "SETTINGS_OPEN_SUCCESS"; return "SETTINGS_OPEN_SUCCESS";
} }
function openWith(tab: string): string {
if (!tab)
return "SETTINGS_OPEN_FAILED: No tab specified";
PopoutService.openSettingsWithTab(tab);
return `SETTINGS_OPEN_SUCCESS: ${tab}`;
}
function close(): string { function close(): string {
PopoutService.closeSettings(); PopoutService.closeSettings();
return "SETTINGS_CLOSE_SUCCESS"; return "SETTINGS_CLOSE_SUCCESS";
@@ -658,11 +665,47 @@ Item {
return "SETTINGS_TOGGLE_SUCCESS"; return "SETTINGS_TOGGLE_SUCCESS";
} }
function toggleWith(tab: string): string {
if (!tab)
return "SETTINGS_TOGGLE_FAILED: No tab specified";
PopoutService.toggleSettingsWithTab(tab);
return `SETTINGS_TOGGLE_SUCCESS: ${tab}`;
}
function focusOrToggle(): string { function focusOrToggle(): string {
PopoutService.focusOrToggleSettings(); PopoutService.focusOrToggleSettings();
return "SETTINGS_FOCUS_OR_TOGGLE_SUCCESS"; return "SETTINGS_FOCUS_OR_TOGGLE_SUCCESS";
} }
function focusOrToggleWith(tab: string): string {
if (!tab)
return "SETTINGS_FOCUS_OR_TOGGLE_FAILED: No tab specified";
PopoutService.focusOrToggleSettingsWithTab(tab);
return `SETTINGS_FOCUS_OR_TOGGLE_SUCCESS: ${tab}`;
}
function tabs(): string {
if (!PopoutService.settingsModal)
return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_widgets\nworkspaces\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout";
var modal = PopoutService.settingsModal;
var ids = [];
var structure = modal.sidebar?.categoryStructure ?? [];
for (var i = 0; i < structure.length; i++) {
var cat = structure[i];
if (cat.separator)
continue;
if (cat.id)
ids.push(cat.id);
if (cat.children) {
for (var j = 0; j < cat.children.length; j++) {
if (cat.children[j].id)
ids.push(cat.children[j].id);
}
}
}
return ids.join("\n");
}
function get(key: string): string { function get(key: string): string {
return JSON.stringify(SettingsData?.[key]); return JSON.stringify(SettingsData?.[key]);
} }

View File

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

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