├── .gitignore
├── configs
├── pinned.json
├── themes
│ └── lotos
│ │ ├── point
│ │ ├── 0.svg
│ │ ├── 1.svg
│ │ ├── 3.svg
│ │ └── 2.svg
│ │ ├── lotos.jsonc
│ │ └── style.css
└── config.jsonc
├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── other.md
│ ├── feature_request.md
│ └── bug_report.md
├── docs
├── tasks
│ ├── ipc
│ │ ├── structs.go
│ │ ├── ipc-req.md
│ │ ├── ipc-main.md
│ │ ├── dockIPC-example.md
│ │ └── dockIPC-task.md
│ ├── cli
│ │ ├── structs.go
│ │ ├── req.go
│ │ ├── out.md
│ │ └── hypr-dock-ctl-example.md
│ ├── test.md
│ └── ipc-main.md
└── customize
│ ├── themes.md
│ └── themes_RU.md
├── internal
├── pkg
│ ├── utils
│ │ ├── common.utils.go
│ │ ├── os.utils.go
│ │ └── gtk.utils.go
│ ├── flags
│ │ └── flags.go
│ ├── signals
│ │ └── signals.go
│ ├── validate
│ │ └── validate.go
│ ├── timer
│ │ └── timer.go
│ ├── desktop
│ │ ├── parse.go
│ │ └── desktop.go
│ ├── popup
│ │ └── popup.go
│ ├── indicator
│ │ └── indicator.go
│ └── cfg
│ │ └── cfg.go
├── itemsctl
│ └── itemsctl.go
├── hysc
│ ├── masks.go
│ ├── utils.go
│ └── hysc.go
├── layerInfo
│ └── layerInfo.go
├── hypr
│ ├── hyprOpt
│ │ └── hypr.option.go
│ └── hyprEvents
│ │ └── events.go
├── placeholders
│ └── placeholders.go
├── detectZone
│ └── detectZone.go
├── btnctl
│ ├── utils.go
│ └── btnctl.go
├── settings
│ └── settings.go
├── state
│ └── state.go
├── layering
│ └── layering.go
├── item
│ ├── popup.go
│ └── item.go
├── app
│ └── app.go
├── pvwidget
│ └── pvwidget.go
└── pvctl
│ └── pvctl.go
├── go.mod
├── Makefile
├── pkg
├── ipc
│ ├── ipc.go
│ ├── getters.go
│ ├── utils.go
│ ├── structs.go
│ └── listeners.go
└── wl
│ ├── app.go
│ └── hyprland_toplevel_export.go
├── main
└── main.go
├── go.sum
├── README_RU.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | configs/pinned.json
3 | project_structure.md
--------------------------------------------------------------------------------
/configs/pinned.json:
--------------------------------------------------------------------------------
1 | {
2 | "Pinned": [
3 | "firefox",
4 | "vlc",
5 | "one.ablaze.floorp"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 | custom: [https://www.donationalerts.com/r/sujerux]
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/other.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Other
3 | about: Any other issue type
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
--------------------------------------------------------------------------------
/configs/themes/lotos/point/0.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/tasks/ipc/structs.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | type Action struct {
4 | Handler func(data ...string) error
5 | NeedsData bool
6 | Usage string
7 | Description string
8 | }
9 |
10 | type Command struct {
11 | Description string
12 | Actions map[string]Action
13 | Default *Action
14 | }
15 |
16 | // var commands map[string]Command
17 |
--------------------------------------------------------------------------------
/docs/tasks/cli/structs.go:
--------------------------------------------------------------------------------
1 | package parse
2 |
3 | type Action struct {
4 | Handler func(data ...string) error
5 | NeedsData bool
6 | Usage string
7 | Description string
8 | }
9 |
10 | type Command struct {
11 | Description string
12 | Actions map[string]Action
13 | Default *Action
14 | }
15 |
16 | // var commands map[string]Command
17 |
--------------------------------------------------------------------------------
/configs/themes/lotos/point/1.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/configs/themes/lotos/point/3.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/configs/themes/lotos/point/2.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/configs/themes/lotos/lotos.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | // Blur window ("true", "false") (default "on")
3 | "Blur": "true",
4 |
5 | // Distance between elements (px) (default 9)
6 | "Spacing": 5,
7 |
8 | // Preview settings
9 | "PreviewStyle": {
10 | // Size (px) (default 120)
11 | "Size": 120,
12 |
13 | // Image/Stream border-radius (px) (default 0)
14 | "BorderRadius": 0,
15 |
16 | // Popup padding (px) (default 10)
17 | "Padding": 10
18 | }
19 | }
--------------------------------------------------------------------------------
/internal/pkg/utils/common.utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | func AddToSlice(slice *[]string, value string) {
4 | *slice = append(*slice, value)
5 | }
6 |
7 | func RemoveFromSliceByValue(slice *[]string, value string) {
8 | index := -1
9 | for i, v := range *slice {
10 | if v == value {
11 | index = i
12 | break
13 | }
14 | }
15 |
16 | if index != -1 {
17 | *slice = append((*slice)[:index], (*slice)[index+1:]...)
18 | }
19 | }
20 |
21 | func RemoveFromSlice(slice []map[string]string, s int) []map[string]string {
22 | return append(slice[:s], slice[s+1:]...)
23 | }
24 |
--------------------------------------------------------------------------------
/internal/pkg/flags/flags.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | import (
4 | "flag"
5 | "os"
6 | )
7 |
8 | type Flags struct {
9 | DevMode bool
10 | Config string
11 | Theme string
12 | }
13 |
14 | func Get(defaultTheme string) Flags {
15 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
16 |
17 | dev := flag.Bool("dev", false, "enable developer mode")
18 | config := flag.String("config", "~/.config/hypr-dock", "config file")
19 | theme := flag.String("theme", defaultTheme, "theme dir")
20 | flag.Parse()
21 |
22 | return Flags{
23 | DevMode: *dev,
24 | Config: *config,
25 | Theme: *theme,
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/docs/tasks/test.md:
--------------------------------------------------------------------------------
1 | #app.gtx
2 | ```tsx
3 | package main
4 |
5 | import "user"
6 |
7 | var users = []string{"Alice", "Bob", "Charlie"}
8 |
9 | func Render() gtk.Widget {
10 | return
11 | {for _, name := range users {
12 |
13 | }}
14 |
15 | }
16 | ```
17 |
18 | #user.gtx
19 | ```tsx
20 | package user
21 |
22 | type Props struct {
23 | Name string
24 | }
25 |
26 | func Render(props Props) gtk.Widget {
27 | return
28 |
29 |
30 | }
31 | ```
--------------------------------------------------------------------------------
/internal/pkg/signals/signals.go:
--------------------------------------------------------------------------------
1 | package signals
2 |
3 | import (
4 | "log"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 |
9 | "github.com/gotk3/gotk3/gtk"
10 | )
11 |
12 | func Handler() {
13 | signalChanel := make(chan os.Signal, 1)
14 | signal.Notify(signalChanel, syscall.SIGTERM, syscall.SIGUSR1)
15 |
16 | go func() {
17 | for {
18 | signalU := <-signalChanel
19 | switch signalU {
20 | case syscall.SIGTERM:
21 | log.Println("Exit... (SIGTERM)")
22 | gtk.MainQuit()
23 | case syscall.SIGUSR1:
24 | log.Println("Exit... (SIGUSR1)")
25 | gtk.MainQuit()
26 | default:
27 | log.Println("Unknow signal")
28 | }
29 | }
30 | }()
31 | }
32 |
--------------------------------------------------------------------------------
/docs/tasks/cli/req.go:
--------------------------------------------------------------------------------
1 | package parse
2 |
3 | import "strings"
4 |
5 | func ParseRequest(request string) (command string, action string, data string, json bool) {
6 | request = strings.TrimSpace(request)
7 | if request == "" {
8 | return "", "", "", false
9 | }
10 |
11 | if strings.HasPrefix(request, "j/") {
12 | json = true
13 | request = request[2:]
14 | request = strings.TrimSpace(request)
15 | }
16 |
17 | parts := strings.SplitN(request, " ", 3)
18 |
19 | if len(parts) > 0 {
20 | command = parts[0]
21 | }
22 | if len(parts) > 1 {
23 | action = parts[1]
24 | }
25 | if len(parts) > 2 {
26 | data = parts[2]
27 | }
28 |
29 | return command, action, data, json
30 | }
31 |
--------------------------------------------------------------------------------
/internal/itemsctl/itemsctl.go:
--------------------------------------------------------------------------------
1 | package itemsctl
2 |
3 | import "hypr-dock/internal/item"
4 |
5 | type List struct {
6 | list map[string]*item.Item
7 | }
8 |
9 | func New() *List {
10 | return &List{
11 | list: make(map[string]*item.Item),
12 | }
13 | }
14 |
15 | func (l *List) GetMap() map[string]*item.Item {
16 | return l.list
17 | }
18 |
19 | func (l *List) Get(className string) *item.Item {
20 | return l.list[className]
21 | }
22 |
23 | func (l *List) Add(className string, item *item.Item) {
24 | l.list[className] = item
25 | }
26 |
27 | func (l *List) Remove(className string) {
28 | delete(l.list, className)
29 | }
30 |
31 | func (l *List) Len() int {
32 | return len(l.list)
33 | }
34 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module hypr-dock
2 |
3 | go 1.24
4 |
5 | toolchain go1.24.3
6 |
7 | require (
8 | github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37
9 | github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774
10 | github.com/goccy/go-json v0.10.3
11 | github.com/gotk3/gotk3 v0.6.3
12 | github.com/hashicorp/go-hclog v1.6.3
13 | github.com/pdf/go-wayland v0.0.2
14 | github.com/pkg/errors v0.9.1
15 | github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a
16 | golang.org/x/sys v0.33.0
17 | )
18 |
19 | require (
20 | github.com/fatih/color v1.13.0 // indirect
21 | github.com/mattn/go-colorable v0.1.12 // indirect
22 | github.com/mattn/go-isatty v0.0.14 // indirect
23 | )
24 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | LOCAL_CONFIG_DIR = $(HOME)/.config/hypr-dock
2 |
3 | PROJECT_BIN_DIR = bin
4 | PROJECT_CONFIG_DIR = configs
5 |
6 | EXECUTABLE = hypr-dock
7 |
8 | install:
9 | sudo cp $(PROJECT_BIN_DIR)/$(EXECUTABLE) /usr/bin/
10 |
11 | mkdir -p $(LOCAL_CONFIG_DIR)
12 | cp -r $(PROJECT_CONFIG_DIR)/* $(LOCAL_CONFIG_DIR)/
13 |
14 | @echo -e "\033[32mInstallation completed."
15 |
16 | uninstall:
17 | sudo rm -f /usr/bin/$(EXECUTABLE)
18 |
19 | rm -rf $(LOCAL_CONFIG_DIR)
20 |
21 | @echo -e "\033[32mInstallation removed."
22 |
23 | update:
24 | sudo rm -f /usr/bin/$(EXECUTABLE)
25 | sudo cp $(PROJECT_BIN_DIR)/$(EXECUTABLE) /usr/bin/
26 |
27 | @echo -e "\033[32mUpdating comleted."
28 |
29 | get:
30 | go mod tidy
31 |
32 | build:
33 | go build -v -o bin/hypr-dock ./main/.
34 |
35 | exec:
36 | ./bin/hypr-dock -dev
37 |
--------------------------------------------------------------------------------
/configs/themes/lotos/style.css:
--------------------------------------------------------------------------------
1 | window {
2 | background-color: transparent;
3 | }
4 |
5 | #app {
6 | background-color: rgba(42, 41, 49, 0.473);
7 | border-radius: 12px;
8 | padding: 5px;
9 | }
10 |
11 | button {
12 | background-color: rgba(0, 0, 0, 0);
13 | padding: 4px;
14 | border-radius: 12px;
15 | border: none;
16 | }
17 |
18 | #menu-item {
19 | padding: 3px;
20 | padding-left: 0;
21 | }
22 |
23 | menu {
24 | border: none;
25 | box-shadow: none;
26 | outline: none;
27 | background-image: none;
28 | }
29 |
30 | menuitem {
31 | padding: 3px 6px;
32 | }
33 |
34 | #pv-item {
35 | background-color: rgba(42, 41, 49, 0.473);
36 | transition: all .2s ease-out;
37 | border-radius: 5px;
38 | border: 1px solid rgba(255, 255, 255, 0.062);
39 | }
40 |
41 | #pv-item.hover {
42 | background-color: rgba(67, 66, 75, 0.301);
43 |
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/docs/customize/themes.md:
--------------------------------------------------------------------------------
1 | ## Open Window Indicators
2 | #### Place indicator images in `~/.config/hypr-dock/themes/[theme_name]/point`.
3 | **Default set:** `0.svg` `1.svg` `2.svg` `3.svg`
4 |
5 | 
6 | 
7 |
8 | ### Customization
9 | - Delete the `point` folder/files to **disable indicators entirely**
10 | - Supported formats: `.svg`, `.png`, `.jpg`, `.webp`
11 | - **Minimum requirement:** Two files (`0.*` and any other)
12 | - Naming must match window counts (e.g. `5.png` for 5 windows)
13 | - Add unlimited files in any order
14 |
15 | ### Logic Example
16 | #### For set: `0.svg` `3.png` `10.jpg`
17 | - **0-2 windows:** Uses `0.svg`
18 | - **3-9 windows:** Uses `3.png`
19 | - **10+ windows:** Uses `10.jpg`
--------------------------------------------------------------------------------
/docs/customize/themes_RU.md:
--------------------------------------------------------------------------------
1 | ## Индикаторы открытых окон
2 | #### Изображения для индикации находятся в `~/.config/hypr-dock/themes/[имя_темы]/point`
3 | **Стандартный набор:** `0.svg` `1.svg` `2.svg` `3.svg`
4 |
5 | 
6 | 
7 |
8 | ### Кастомизация
9 | - Удалите папку `point` или файлы в ней, чтобы **полностью отключить индикаторы**
10 | - Поддерживаемые форматы: `.svg`, `.png`, `.jpg`, `.webp`
11 | - **Минимальные требования:** Два файла (`0.*` и любой другой)
12 | - Имена должны соответствовать количеству окон (например `5.png` для 5 окон)
13 | - Можно добавлять неограниченное количество файлов в любом порядке
14 |
15 | ### Пример работы
16 | #### Для набора: `0.svg` `3.png` `10.jpg`
17 | - **0-2 окна:** Используется `0.svg`
18 | - **3-9 окон:** Используется `3.png`
19 | - **10+ окон:** Используется `10.jpg`
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for Hypr-dock
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Feature request
11 |
12 | ### Description
13 |
14 |
15 | ### Use case
16 |
17 |
18 | ### Proposed solution
19 |
20 |
21 | ### Alternatives considered
22 |
23 |
24 | ### Additional context
25 | Add any other context or screenshots about the feature request here. This could include:
26 | - Links to relevant documentation or discussions
27 | - Screenshots or mockups
28 | - Any other relevant information
29 |
30 |
--------------------------------------------------------------------------------
/internal/hysc/masks.go:
--------------------------------------------------------------------------------
1 | package hysc
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/gotk3/gotk3/gdk"
7 | )
8 |
9 | func ApplyVector(p *gdk.Pixbuf, pos Cord, size Size, mask func(Cord) float64) {
10 | if p.GetNChannels() != 4 {
11 | return
12 | }
13 |
14 | pixels := p.GetPixels()
15 | stride := p.GetRowstride()
16 | width, height := p.GetWidth(), p.GetHeight()
17 |
18 | x0 := max(0, pos.X)
19 | y0 := max(0, pos.Y)
20 | x1 := min(pos.X+size.W, width)
21 | y1 := min(pos.Y+size.H, height)
22 |
23 | for y := y0; y < y1; y++ {
24 | rowOffset := y * stride
25 | relY := y - pos.Y
26 |
27 | for x := x0; x < x1; x++ {
28 | alpha := mask(Cord{x - pos.X, relY})
29 | offset := rowOffset + x*4 + 3
30 | pixels[offset] = uint8(alpha * 255)
31 | }
32 | }
33 | }
34 |
35 | func radiusmask(pixel Cord, center Cord, R float64) float64 {
36 | dx := float64(pixel.X - center.X)
37 | dy := float64(pixel.Y - center.Y)
38 | distance := math.Sqrt(dx*dx + dy*dy)
39 |
40 | if distance <= R-1.0 {
41 | return 1.0
42 | }
43 | if distance >= R+1.0 {
44 | return 0.0
45 | }
46 |
47 | t := (distance - (R - 1.0)) / 2.0
48 | return 1.0 - t*t
49 | }
50 |
--------------------------------------------------------------------------------
/internal/pkg/utils/os.utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "os"
7 | "os/exec"
8 | "strings"
9 | )
10 |
11 | func Launch(command string) {
12 | cmd := exec.Command("sh", "-c", command)
13 | log.Printf("Launching command: %s\n", command)
14 |
15 | if err := cmd.Start(); err != nil {
16 | log.Printf("Unable to launch command: %s, error: %v\n", command, err)
17 | }
18 | }
19 |
20 | func LoadTextFile(path string) ([]string, error) {
21 | bytes, err := os.ReadFile(path)
22 | if err != nil {
23 | return nil, err
24 | }
25 | lines := strings.Split(string(bytes), "\n")
26 | var output []string
27 | for _, line := range lines {
28 | line = strings.TrimSpace(line)
29 | if line != "" {
30 | output = append(output, line)
31 | }
32 |
33 | }
34 | return output, nil
35 | }
36 |
37 | func TempDir() string {
38 | if os.Getenv("TMPDIR") != "" {
39 | return os.Getenv("TMPDIR")
40 | } else if os.Getenv("TEMP") != "" {
41 | return os.Getenv("TEMP")
42 | } else if os.Getenv("TMP") != "" {
43 | return os.Getenv("TMP")
44 | }
45 | return "/tmp"
46 | }
47 |
48 | func FileExists(path string) bool {
49 | _, err := os.Stat(path)
50 | return !errors.Is(err, os.ErrNotExist)
51 | }
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report an issue with Hypr-dock
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 |
12 |
13 | **To reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | 1. Start hypr-dock with command `hypr-dock`
17 | 2. Click something
18 | 3. Something is broken
19 |
20 | **Expected behavior**
21 |
22 |
23 | **Setup information:**
24 | - Operating System: [e.g. Arch Linux, Ubuntu 22.10]
25 | - Output of `hyprctl version`:
26 |
27 | ```
28 |
29 | ```
30 | - Hypr-dock version or commit: [e.g. 0.1.0]
31 |
32 | **Configuration**
33 |
34 |
35 | Main config
36 |
37 | ```jsonc
38 |
39 | ```
40 |
41 |
42 |
43 |
44 | Themes
45 |
46 | ```jsonc
47 |
48 | ```
49 |
50 |
51 |
52 | **Additional context**
53 |
54 |
55 | **Screenshots**
56 |
57 |
58 |
--------------------------------------------------------------------------------
/internal/hysc/utils.go:
--------------------------------------------------------------------------------
1 | package hysc
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/gotk3/gotk3/gdk"
10 | )
11 |
12 | func getHandle(address string) (uint64, error) {
13 | prefix := "0x"
14 |
15 | address = strings.TrimPrefix(address, prefix)
16 |
17 | handle, err := strconv.ParseUint(address, 16, 64)
18 | if err != nil {
19 | return 0, fmt.Errorf("failed to parse address: %v", err)
20 | }
21 |
22 | return handle, nil
23 | }
24 |
25 | func nRGBAtoPixbuf(img *image.NRGBA) (*gdk.Pixbuf, error) {
26 | width := img.Bounds().Dx()
27 | height := img.Bounds().Dy()
28 |
29 | pixbuf, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, true, 8, width, height)
30 | if err != nil {
31 | return nil, err
32 | }
33 |
34 | pixels := pixbuf.GetPixels()
35 |
36 | for y := 0; y < height; y++ {
37 | for x := 0; x < width; x++ {
38 | srcOffset := y*img.Stride + x*4
39 | dstOffset := (y*width + x) * 4
40 |
41 | copy(
42 | pixels[dstOffset:dstOffset+4],
43 | img.Pix[srcOffset:srcOffset+4],
44 | )
45 | }
46 | }
47 |
48 | return pixbuf, nil
49 | }
50 |
51 | func min(a, b int) int {
52 | if a < b {
53 | return a
54 | }
55 | return b
56 | }
57 |
58 | func max(a, b int) int {
59 | if a > b {
60 | return a
61 | }
62 | return b
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/ipc/ipc.go:
--------------------------------------------------------------------------------
1 | package ipc
2 |
3 | import (
4 | "log"
5 | "net"
6 | "time"
7 | )
8 |
9 | func Hyprctl(cmd string) (response []byte, err error) {
10 | conn, err := net.Dial("unix", getUnixSockAdress())
11 | if err != nil {
12 | return nil, err
13 | }
14 |
15 | message := []byte(cmd)
16 | _, err = conn.Write(message)
17 | if err != nil {
18 | return nil, err
19 | }
20 |
21 | response = make([]byte, 102400)
22 | n, err := conn.Read(response)
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | defer conn.Close()
28 |
29 | return response[:n], nil
30 | }
31 |
32 | func InitHyprEvents() {
33 | for {
34 | unixConnect, err := net.DialUnix("unix", nil, getUnixSock2Adress())
35 | if err != nil {
36 | log.Printf("Failed to connect to Unix socket: %v. Retrying in 5 seconds...", err)
37 | time.Sleep(5 * time.Second)
38 | continue
39 | }
40 | defer unixConnect.Close()
41 |
42 | for {
43 | buffer := make([]byte, 10240)
44 | unixNumber, err := unixConnect.Read(buffer)
45 | if err != nil {
46 | log.Printf("Error reading from Unix socket: %v. Reconnecting...", err)
47 | break
48 | }
49 |
50 | events := splitEvent(string(buffer[:unixNumber]))
51 | for _, event := range events {
52 | go DispatchEvent(event)
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/internal/pkg/validate/validate.go:
--------------------------------------------------------------------------------
1 | package validate
2 |
3 | import (
4 | "log"
5 | "slices"
6 | )
7 |
8 | func Preview(value string, runtime bool) bool {
9 | validList := []string{"live", "static", "none"}
10 | return Allowed("Preview", value, validList, runtime, true)
11 | }
12 |
13 | func Layer(value string, runtime bool) bool {
14 | validList := []string{"auto", "exclusive-top", "exclusive-bottom", "background", "bottom", "top", "overlay"}
15 | return Allowed("Layer", value, validList, runtime, true)
16 | }
17 |
18 | func Position(value string, runtime bool) bool {
19 | validList := []string{"left", "right", "top", "bottom"}
20 | return Allowed("Position", value, validList, runtime, true)
21 | }
22 |
23 | func Blur(value string, runtime bool) bool {
24 | validList := []string{"true", "false"}
25 | return Allowed("Blur", value, validList, runtime, false)
26 | }
27 |
28 | func SystemGapUsed(value string, runtime bool) bool {
29 | validList := []string{"true", "false"}
30 | return Allowed("SystemGapUsed", value, validList, runtime, true)
31 | }
32 |
33 | func Allowed[T comparable](key string, value T, validList []T, runtime bool, logs bool) bool {
34 | if slices.Contains(validList, value) {
35 | return true
36 | }
37 |
38 | if !logs {
39 | return false
40 | }
41 |
42 | if runtime {
43 | log.Printf("%s \"%v\" is incorrect or empty", key, value)
44 | return false
45 | }
46 |
47 | log.Printf("%s \"%v\" is incorrect or empty. Default value will be used", key, value)
48 | return false
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/ipc/getters.go:
--------------------------------------------------------------------------------
1 | package ipc
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/goccy/go-json"
8 | )
9 |
10 | func GetMonitors() ([]Monitor, error) {
11 | var monitors []Monitor
12 | response, err := Hyprctl("j/monitors")
13 | if err != nil {
14 | return nil, err
15 | }
16 | err = json.Unmarshal([]byte(response), &monitors)
17 | return monitors, err
18 | }
19 |
20 | func GetClients() ([]Client, error) {
21 | var clients []Client
22 | response, err := Hyprctl("j/clients")
23 | if err != nil {
24 | return nil, err
25 | }
26 | err = json.Unmarshal([]byte(response), &clients)
27 | return clients, err
28 | }
29 |
30 | func GetActiveWindow() (*Client, error) {
31 | var activeWindow Client
32 | response, err := Hyprctl("j/activewindow")
33 | if err != nil {
34 | log.Printf("Failed to get active window: %v", err)
35 | return nil, err
36 | }
37 | err = json.Unmarshal([]byte(response), &activeWindow)
38 | if err != nil {
39 | log.Printf("Failed to unmarshal active window: %v", err)
40 | return nil, err
41 | }
42 | return &activeWindow, nil
43 | }
44 |
45 | func GetOption(option string, v interface{}) error {
46 | cmd := fmt.Sprintf("j/getoption %s", option)
47 | response, err := Hyprctl(cmd)
48 | if err != nil {
49 | log.Printf("Failed to execute Hyprctl command for option '%s': %v", option, err)
50 | return err
51 | }
52 | err = json.Unmarshal(response, v)
53 | if err != nil {
54 | log.Printf("Failed to unmarshal JSON response for option '%s': %v", option, err)
55 | return err
56 | }
57 | return nil
58 | }
59 |
--------------------------------------------------------------------------------
/internal/layerInfo/layerInfo.go:
--------------------------------------------------------------------------------
1 | package layerinfo
2 |
3 | import (
4 | "fmt"
5 | "hypr-dock/pkg/ipc"
6 |
7 | "github.com/goccy/go-json"
8 | )
9 |
10 | type Layer struct {
11 | Address string `json:"address"`
12 | X int `json:"x"`
13 | Y int `json:"y"`
14 | W int `json:"w"`
15 | H int `json:"h"`
16 | Namespace string `json:"namespace"`
17 | Pid int `json:"pid"`
18 |
19 | Monitor string
20 | Layer string
21 | }
22 |
23 | type Monitor struct {
24 | Levels map[string][]Layer `json:"levels"`
25 | }
26 |
27 | func GetDock() (*Layer, error) {
28 | return Get("hypr-dock")
29 | }
30 |
31 | func Get(namespace string) (*Layer, error) {
32 | jsonData, err := ipc.Hyprctl("j/layers")
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | var monitors map[string]Monitor
38 |
39 | err = json.Unmarshal(jsonData, &monitors)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | for monitorName, monitor := range monitors {
45 | for layerName, layers := range monitor.Levels {
46 | for _, layer := range layers {
47 | if layer.Namespace == namespace {
48 | layer.Monitor = monitorName
49 | layer.Layer = layerName
50 | return &layer, nil
51 | }
52 | }
53 | }
54 | }
55 |
56 | return nil, fmt.Errorf("%s layer not found", namespace)
57 | }
58 |
59 | func GetMonitor() *ipc.Monitor {
60 | dock, _ := GetDock()
61 | monitors, _ := ipc.GetMonitors()
62 | for _, monitor := range monitors {
63 | if monitor.Name == dock.Monitor {
64 | return &monitor
65 | }
66 | }
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/ipc/utils.go:
--------------------------------------------------------------------------------
1 | package ipc
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "net"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 | )
11 |
12 | func SearchClientByAddress(address string) (Client, error) {
13 | clients, err := GetClients()
14 | if err != nil {
15 | log.Println(err)
16 | return Client{}, err
17 | }
18 |
19 | for _, ipcClient := range clients {
20 | if ipcClient.Address == address {
21 | return ipcClient, nil
22 | }
23 | }
24 |
25 | err = errors.New("Client non found by address: " + address)
26 | return Client{}, err
27 | }
28 |
29 | func getHyprPathes() (XDGRuntimeDirHypr string, HIS string) {
30 | XDGRuntimeDirHypr = filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "hypr")
31 | HIS = os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
32 | return XDGRuntimeDirHypr, HIS
33 | }
34 |
35 | func getUnixSockAdress() (unixSockAdress string) {
36 | XDGRuntimeDirHypr, HIS := getHyprPathes()
37 |
38 | return filepath.Join(XDGRuntimeDirHypr, HIS, ".socket.sock")
39 | }
40 |
41 | func getUnixSock2Adress() (unixSock2Adress *net.UnixAddr) {
42 | XDGRuntimeDirHypr, HIS := getHyprPathes()
43 |
44 | return &net.UnixAddr{
45 | Name: filepath.Join(XDGRuntimeDirHypr, HIS, ".socket2.sock"),
46 | Net: "unix",
47 | }
48 | }
49 |
50 | func splitEvent(multiLineEvent string) []string {
51 | events := strings.Split(multiLineEvent, "\n")
52 |
53 | var filteredEvents []string
54 | for _, event := range events {
55 | event = strings.TrimSpace(event)
56 | if event != "" {
57 | filteredEvents = append(filteredEvents, event)
58 | }
59 | }
60 |
61 | return filteredEvents
62 | }
63 |
--------------------------------------------------------------------------------
/main/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strconv"
8 | "syscall"
9 |
10 | "github.com/allan-simon/go-singleinstance"
11 | "github.com/gotk3/gotk3/gtk"
12 |
13 | "hypr-dock/internal/app"
14 | "hypr-dock/internal/hypr/hyprEvents"
15 | "hypr-dock/internal/layering"
16 | "hypr-dock/internal/pkg/signals"
17 | "hypr-dock/internal/pkg/utils"
18 | "hypr-dock/internal/settings"
19 | "hypr-dock/internal/state"
20 | )
21 |
22 | func main() {
23 | signals.Handler()
24 |
25 | lockFilePath := fmt.Sprintf("%s/hypr-dock-%s.lock", utils.TempDir(), os.Getenv("USER"))
26 | lockFile, err := singleinstance.CreateLockFile(lockFilePath)
27 | if err != nil {
28 | file, err := utils.LoadTextFile(lockFilePath)
29 | if err == nil {
30 | pidStr := file[0]
31 | pidInt, _ := strconv.Atoi(pidStr)
32 | syscall.Kill(pidInt, syscall.SIGUSR1)
33 | }
34 | os.Exit(0)
35 | }
36 | defer lockFile.Close()
37 |
38 | // window build
39 | settings, err := settings.Init()
40 | if err != nil {
41 | log.Println("Settings init error: ", err)
42 | }
43 |
44 | gtk.Init(nil)
45 |
46 | appState := state.New(settings)
47 |
48 | window, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
49 | if err != nil {
50 | log.Fatal("Unable to create window:", err)
51 | }
52 | appState.SetWindow(window)
53 |
54 | window.SetTitle("hypr-dock")
55 |
56 | layering.SetWindowProperty(appState)
57 |
58 | err = utils.AddCssProvider(settings.CurrentThemeStylePath)
59 | if err != nil {
60 | log.Println("CSS file not found, the default GTK theme is running!\n", err)
61 | }
62 |
63 | app := app.BuildApp(appState)
64 |
65 | window.Add(app)
66 | window.Connect("destroy", func() { gtk.MainQuit() })
67 | window.ShowAll()
68 |
69 | // post
70 | hyprEvents.Init(appState)
71 |
72 | // end
73 | gtk.Main()
74 | }
75 |
--------------------------------------------------------------------------------
/internal/hypr/hyprOpt/hypr.option.go:
--------------------------------------------------------------------------------
1 | package hyprOpt
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "hypr-dock/pkg/ipc"
7 | "log"
8 | "math"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | type gapsOut struct {
14 | Option string `json:"option"`
15 | Custom string `json:"custom"`
16 | Set bool `json:"set"`
17 | }
18 |
19 | func GetGap() ([]int, error) {
20 | option := "general:gaps_out"
21 |
22 | gapsVal := gapsOut{}
23 | err := ipc.GetOption(option, &gapsVal)
24 | if err != nil {
25 | err = fmt.Errorf("failed to get option \"%s\": %v", option, err)
26 | log.Println(err)
27 | return nil, err
28 | }
29 |
30 | if !gapsVal.Set {
31 | errorText := fmt.Sprintf("value \"%s\" is unset", option)
32 | log.Println(errorText)
33 | return nil, errors.New(errorText)
34 | }
35 |
36 | if gapsVal.Custom == "" {
37 | errorText := fmt.Sprintf("value \"%s\" is empty", option)
38 | log.Println(errorText)
39 | return nil, errors.New(errorText)
40 | }
41 |
42 | outValues := []int{}
43 | gapsVal.Custom = strings.TrimSpace(gapsVal.Custom)
44 | values := strings.Split(gapsVal.Custom, " ")
45 | for _, value := range values {
46 | intValue, err := strconv.ParseFloat(value, 64)
47 | if err != nil {
48 | err = fmt.Errorf("failed to convert \"%s\" to int: %v", value, err)
49 | log.Println(err)
50 | return nil, err
51 | }
52 |
53 | outValues = append(outValues, int(math.Round(math.Max(intValue, 0))))
54 | }
55 |
56 | return outValues, nil
57 | }
58 |
59 | func GapChangeEvent(handler func(gap int)) {
60 | var preGap int
61 | ipc.AddEventListener("configreloaded", func(e string) {
62 | gaps, err := GetGap()
63 | if err != nil {
64 | log.Println("Reading gap error", err)
65 | return
66 | }
67 |
68 | if gaps[0] == preGap {
69 | return
70 | }
71 |
72 | preGap = gaps[0]
73 | handler(gaps[0])
74 | }, true)
75 | }
76 |
--------------------------------------------------------------------------------
/configs/config.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "CurrentTheme": "lotos",
3 |
4 | // Icon size (px) (default 23)
5 | "IconSize": 23,
6 |
7 | // Window overlay layer height (auto, exclusive-top, exclusive-bottom, background, bottom, top, overlay) (default "auto")
8 | "Layer": "exclusive-bottom",
9 |
10 | // Window position on screen (top, bottom, left, right) (default "bottom")
11 | "Position": "bottom",
12 |
13 | // Delay before hiding the dock (ms) (default 400)
14 | "AutoHideDeley": 400, // *Only for "Layer": "auto"*
15 |
16 | // Use system gap (true, false) (default "true")
17 | "SystemGapUsed": "true",
18 |
19 | // Indent from the edge of the screen (px) (default 8)
20 | "Margin": 8,
21 |
22 | // Distance of the context menu from the window (px) (default 0)
23 | "ContextPos": 5,
24 |
25 |
26 | // Window thumbnail mode selection (none, live, static) (default "none")
27 | "Preview": "none",
28 | /*
29 | "none" - disabled (text menus)
30 | "static" - last window frame (stable)
31 | "live" - window streaming (unstable) !EXPEREMENTAL!
32 |
33 | !WARNING!
34 | BY SETTING "Preview" TO "live" OR "static", YOU AGREE TO THE CAPTURE
35 | OF WINDOW CONTENTS.
36 | THE "HYPR-DOCK" PROGRAM DOES NOT COLLECT, STORE, OR TRANSMIT ANY DATA.
37 | WINDOW CAPTURE OCCURS ONLY FOR THE DURATION OF THE THUMBNAIL DISPLAY!
38 |
39 | Source code: https://github.com/lotos-linux/hypr-dock
40 | */
41 |
42 | "PreviewAdvanced": {
43 | // Live preview fps (0 - ∞) (default 30)
44 | "FPS": 30,
45 |
46 | // Live preview bufferSize (1 - 20) (default 5)
47 | "BufferSize": 5,
48 |
49 | // Popup show/hide/move delays (ms)
50 | "ShowDelay": 600, // (default 600)
51 | "HideDelay": 300, // (default 300)
52 | "MoveDelay": 200 // (default 200)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/docs/tasks/cli/out.md:
--------------------------------------------------------------------------------
1 | # **Спецификация текстового формата ответов**
2 |
3 | ## **1. Форматы ответов**
4 |
5 | ### **1.1 Успешный ответ с данными**
6 | ```
7 | <ключ>: <значение>
8 | <вложенный_ключ>:
9 | <подключ>: <значение>
10 | ```
11 |
12 | **Пример:**
13 | ```
14 | user:
15 | id: 42
16 | name: Ivan Petrov
17 | permissions:
18 | - read
19 | - write
20 | ```
21 |
22 | ### **1.2 Успешный ответ без данных**
23 | ```
24 | ok: <сообщение>
25 | ```
26 |
27 | **Пример:**
28 | ```
29 | ok: settings updated successfully
30 | ```
31 |
32 | ### **1.3 Ответ с ошибкой**
33 | ```
34 | error: <текст ошибки> [<код>]
35 | ```
36 |
37 | **Пример:**
38 | ```
39 | error: file not found [1]
40 | ```
41 |
42 | ## **2. Таблица стандартных форматов**
43 |
44 | | Тип ответа | Формат вывода | Примеры |
45 | |---------------------|-----------------------------------|----------------------------------|
46 | | Успех с данными | Ключ-значение с табами | `id: 42`
`name: Ivan` |
47 | | Успех без данных | `ok: <сообщение>` | `ok: operation completed` |
48 | | Ошибка | `error: <текст> [<код>]` | `error: invalid input [2]` |
49 | | Вложенные данные | С отступом 4 пробела | `settings:`
` theme: dark` |
50 | | Списки значений | С дефисом и отступом | `- read`
`- write` |
51 |
52 | ## **3. Особенности форматирования**
53 |
54 | 1. **Отступы**:
55 | - Каждый уровень вложенности: 4 пробела
56 | - Выравнивание значений после двоеточия
57 |
58 | 2. **Списки**:
59 | - Каждый элемент с новой строки
60 | - Префикс в виде дефиса и пробела
61 |
62 | 3. **Ошибки**:
63 | - Всегда начинаются с `error: `
64 | - Код ошибки в квадратных скобках
65 |
66 | ## **4. Примеры вывода**
67 |
68 | ### **Комплексные данные**
69 | ```
70 | server:
71 | id: srv-01
72 | status: active
73 | ips:
74 | - 192.168.1.1
75 | - 10.0.0.1
76 | config:
77 | max_connections: 100
78 | timeout: 30
79 | ```
80 |
81 | ### **Простое подтверждение**
82 | ```
83 | ok: cache cleared
84 | ```
85 |
86 | ### **Ошибка с кодом**
87 | ```
88 | error: connection timeout [5]
89 | ```
--------------------------------------------------------------------------------
/internal/pkg/timer/timer.go:
--------------------------------------------------------------------------------
1 | package timer
2 |
3 | import (
4 | "sync"
5 | "time"
6 | )
7 |
8 | type Timer struct {
9 | timer *time.Timer
10 | active bool
11 | startTime time.Time
12 | mu sync.Mutex
13 | handler func() // текущий handler (только для активного таймера)
14 | duration time.Duration // текущая длительность (только для активного таймера)
15 | }
16 |
17 | func New() *Timer {
18 | return &Timer{}
19 | }
20 |
21 | func (t *Timer) Run(ms int, handler func()) {
22 | t.mu.Lock()
23 | defer t.mu.Unlock()
24 |
25 | t.stopUnsafe() // останавливаем предыдущий таймер, если был
26 |
27 | t.active = true
28 | t.startTime = time.Now()
29 | t.handler = handler
30 | t.duration = time.Duration(ms) * time.Millisecond
31 |
32 | t.timer = time.AfterFunc(t.duration, func() {
33 | t.mu.Lock()
34 | defer t.mu.Unlock()
35 |
36 | if t.active {
37 | t.active = false
38 | t.handler()
39 | }
40 | })
41 | }
42 |
43 | func (t *Timer) Stop() {
44 | t.mu.Lock()
45 | defer t.mu.Unlock()
46 | t.stopUnsafe()
47 | }
48 |
49 | func (t *Timer) stopUnsafe() {
50 | if !t.active {
51 | return
52 | }
53 |
54 | t.timer.Stop()
55 | t.active = false
56 | // Очищаем временные данные
57 | t.handler = nil
58 | t.duration = 0
59 | }
60 |
61 | func (t *Timer) ExecIf(check func(elapsedMs int) bool) bool {
62 | t.mu.Lock()
63 | defer t.mu.Unlock()
64 |
65 | if !t.active {
66 | return false
67 | }
68 |
69 | elapsed := time.Since(t.startTime)
70 | if check(int(elapsed.Milliseconds())) {
71 | t.triggerUnsafe()
72 | return true
73 | }
74 | return false
75 | }
76 |
77 | func (t *Timer) ExecNow() bool {
78 | t.mu.Lock()
79 | defer t.mu.Unlock()
80 |
81 | if !t.active {
82 | return false
83 | }
84 |
85 | t.triggerUnsafe()
86 | return true
87 | }
88 |
89 | func (t *Timer) IsRunning() bool {
90 | t.mu.Lock()
91 | defer t.mu.Unlock()
92 | return t.active
93 | }
94 |
95 | func (t *Timer) triggerUnsafe() {
96 | if !t.active || t.handler == nil {
97 | return
98 | }
99 |
100 | t.timer.Stop()
101 | t.active = false
102 | handler := t.handler
103 | // Очищаем временные данные перед вызовом handler
104 | t.handler = nil
105 | t.duration = 0
106 |
107 | handler()
108 | }
109 |
--------------------------------------------------------------------------------
/docs/tasks/ipc/ipc-req.md:
--------------------------------------------------------------------------------
1 | # **Стандарт ответов через IPC-сокет**
2 |
3 | ## **1. Форматы ответов**
4 |
5 | ### **1.1 Успешный ответ**
6 | #### Текстовый режим (без `j/`):
7 | ```
8 | <ключ>: <значение>
9 | <вложенный_ключ>:
10 | <поле>: <значение>
11 | ```
12 | Пример:
13 | ```
14 | workspace: main
15 | active_window:
16 | id: 42
17 | title: Terminal
18 | ```
19 |
20 | #### JSON-режим (с `j/`):
21 | ```json
22 | {
23 | "<ключ>": "<значение>",
24 | "<вложенный_ключ>": {
25 | "<поле>": "<значение>"
26 | }
27 | }
28 | ```
29 |
30 | ### **1.2 Ответ с ошибкой**
31 | #### Текстовый режим:
32 | ```
33 | error: <описание_ошибки> [<код>]
34 | ```
35 | Где `<код>` - числовой код для os.Exit()
36 |
37 | Пример:
38 | ```
39 | error: Invalid command [1]
40 | ```
41 |
42 | #### JSON-режим:
43 | ```json
44 | {
45 | "error": "<описание_ошибки>",
46 | "code": <числовой_код>
47 | }
48 | ```
49 | Где `code` - числовой код для os.Exit()
50 |
51 | Пример:
52 | ```json
53 | {
54 | "error": "Invalid command",
55 | "code": 1
56 | }
57 | ```
58 |
59 | ## **2. Коды завершения**
60 |
61 | Коды ошибок соответствуют стандартным значениям для os.Exit():
62 | - `0` - Успешное выполнение
63 | - `1` - Общая ошибка
64 | - `2` - Ошибка в аргументах
65 | - `3` - Ошибка доступа
66 | - `4` - Ресурс не найден
67 |
68 | ## **3. Примеры взаимодействия**
69 |
70 | ### Успешный запрос (текст):
71 | ```
72 | get workspace
73 | →
74 | workspace: main
75 | ```
76 |
77 | ### Ошибка (текст):
78 | ```
79 | get invalid
80 | →
81 | error: Not found [4]
82 | ```
83 |
84 | ### Успешный запрос (JSON):
85 | ```
86 | j/ get workspace
87 | →
88 | {"workspace":"main"}
89 | ```
90 |
91 | ### Ошибка (JSON):
92 | ```
93 | j/ get invalid
94 | →
95 | {"error":"Not found","code":4}
96 | ```
97 |
98 | ## **4. Особенности**
99 |
100 | 1. При успешном выполнении:
101 | - В текстовом режиме возвращаются только данные
102 | - В JSON-режиме возвращается только объект с данными
103 |
104 | 2. При ошибке:
105 | - Всегда содержит описание ошибки
106 | - Всегда содержит числовой код для os.Exit()
107 | - Формат един для всех типов ошибок
108 |
109 | 3. Совместимость:
110 | - Прямой доступ к сокету возвращает данные в чистом виде
111 | - CLI-утилита использует коды для os.Exit()
--------------------------------------------------------------------------------
/internal/hypr/hyprEvents/events.go:
--------------------------------------------------------------------------------
1 | package hyprEvents
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strings"
7 |
8 | "github.com/gotk3/gotk3/glib"
9 |
10 | "hypr-dock/internal/app"
11 | "hypr-dock/internal/state"
12 | "hypr-dock/pkg/ipc"
13 | )
14 |
15 | func Init(appState *state.State) {
16 | ipc.AddEventListener("windowtitlev2", func(event string) {
17 | windowTitleHandler(event, appState)
18 | }, true)
19 |
20 | ipc.AddEventListener("openwindow", func(event string) {
21 | openwindowHandler(event, appState)
22 | }, true)
23 |
24 | ipc.AddEventListener("closewindow", func(event string) {
25 | closewindowHandler(event, appState)
26 | }, true)
27 |
28 | ipc.AddEventListener("activespecial", func(event string) {
29 | activatespecialHandler(event, appState)
30 | }, true)
31 |
32 | go ipc.InitHyprEvents()
33 | }
34 |
35 | func windowTitleHandler(event string, appState *state.State) {
36 | data := eventHandler(event, 2)
37 | address := "0x" + strings.TrimSpace(data[0])
38 | go app.ChangeWindowTitle(address, data[1], appState)
39 | }
40 |
41 | func activatespecialHandler(event string, appState *state.State) {
42 | data := eventHandler(event, 2)
43 | log.Printf("Received activespecial event: %v", data)
44 |
45 | if data[0] == "special:special" {
46 | log.Println("Special workspace activated")
47 | appState.SetSpecial(true)
48 | } else {
49 | log.Println("Special workspace deactivated")
50 | appState.SetSpecial(false)
51 | }
52 | }
53 |
54 | func openwindowHandler(event string, appState *state.State) {
55 | data := eventHandler(event, 4)
56 | address := "0x" + strings.TrimSpace(data[0])
57 | windowClient, err := ipc.SearchClientByAddress(address)
58 | if err != nil {
59 | fmt.Println(err)
60 | } else {
61 | glib.IdleAdd(func() {
62 | app.InitNewItemInIPC(windowClient, appState)
63 | })
64 | }
65 | }
66 |
67 | func closewindowHandler(event string, appState *state.State) {
68 | data := eventHandler(event, 1)
69 | address := "0x" + strings.TrimSpace(data[0])
70 | glib.IdleAdd(func() {
71 | app.RemoveApp(address, appState)
72 | })
73 | }
74 |
75 | func eventHandler(event string, n int) []string {
76 | parts := strings.SplitN(event, ">>", 2)
77 | dataClast := strings.TrimSpace(parts[1])
78 | dataParts := strings.SplitN(dataClast, ",", n)
79 |
80 | for i := range dataParts {
81 | dataParts[i] = strings.TrimSpace(dataParts[i])
82 | }
83 |
84 | return dataParts
85 | }
86 |
--------------------------------------------------------------------------------
/internal/placeholders/placeholders.go:
--------------------------------------------------------------------------------
1 | package placeholders
2 |
3 | import (
4 | "fmt"
5 | "hypr-dock/internal/pkg/utils"
6 | "log"
7 | "strings"
8 | )
9 |
10 | var desktopPlaceholders = map[string]bool{
11 | "%f": true,
12 | "%F": true,
13 | "%u": true,
14 | "%U": true,
15 | "%d": true,
16 | "%D": true,
17 | "%n": true,
18 | "%N": true,
19 | "%i": true,
20 | "%c": true,
21 | "%k": true,
22 | "%v": true,
23 | "%m": true,
24 | }
25 |
26 | func Run(command string) error {
27 | cleanCommand, err := Clean(command)
28 | if err != nil {
29 | log.Println(err)
30 | return err
31 | }
32 |
33 | fmt.Println(command, " ", cleanCommand)
34 | utils.Launch(cleanCommand)
35 | return nil
36 | }
37 |
38 | func Clean(execLine string) (string, error) {
39 | args, err := splitCommandLine(execLine)
40 | if err != nil {
41 | return "", fmt.Errorf("failed to parse command line: %w", err)
42 | }
43 |
44 | var filteredArgs []string
45 | for _, arg := range args {
46 | if !containsPlaceholder(arg) {
47 | filteredArgs = append(filteredArgs, arg)
48 | }
49 | }
50 |
51 | return strings.Join(filteredArgs, " "), nil
52 | }
53 |
54 | func containsPlaceholder(s string) bool {
55 | for i := 0; i < len(s)-1; i++ {
56 | if s[i] == '%' {
57 | placeholder := s[i : i+2]
58 | if desktopPlaceholders[placeholder] {
59 | return true
60 | }
61 | }
62 | }
63 | return false
64 | }
65 |
66 | func splitCommandLine(line string) ([]string, error) {
67 | var args []string
68 | var currentArg strings.Builder
69 | inQuotes := false
70 | escapeNext := false
71 |
72 | for i, r := range line {
73 | if escapeNext {
74 | currentArg.WriteRune(r)
75 | escapeNext = false
76 | continue
77 | }
78 |
79 | switch r {
80 | case '\\':
81 | if i < len(line)-1 {
82 | escapeNext = true
83 | }
84 | case '"':
85 | inQuotes = !inQuotes
86 | case ' ', '\t', '\n', '\r':
87 | if inQuotes {
88 | currentArg.WriteRune(r)
89 | } else {
90 | if currentArg.Len() > 0 {
91 | args = append(args, currentArg.String())
92 | currentArg.Reset()
93 | }
94 | }
95 | default:
96 | currentArg.WriteRune(r)
97 | }
98 | }
99 |
100 | if currentArg.Len() > 0 {
101 | args = append(args, currentArg.String())
102 | }
103 |
104 | if inQuotes {
105 | return nil, fmt.Errorf("unclosed quotes in command line")
106 | }
107 |
108 | return args, nil
109 | }
110 |
--------------------------------------------------------------------------------
/pkg/ipc/structs.go:
--------------------------------------------------------------------------------
1 | package ipc
2 |
3 | type Workspace struct {
4 | Id int `json:"id"`
5 | Name string `json:"name"`
6 | Monitor string `json:"monitor"`
7 | Windows int `json:"windows"`
8 | Hasfullscreen bool `json:"hasfullscreen"`
9 | Lastwindow string `json:"lastwindow"`
10 | Lastwindowtitle string `json:"lastwindowtitle"`
11 | }
12 |
13 | type Monitor struct {
14 | Id int `json:"id"`
15 | Name string `json:"name"`
16 | Description string `json:"description"`
17 | Make string `json:"make"`
18 | Model string `json:"model"`
19 | Serial string `json:"serial"`
20 | Width int `json:"width"`
21 | Height int `json:"height"`
22 | RefreshRate float64 `json:"refreshRate"`
23 | X int `json:"x"`
24 | Y int `json:"y"`
25 |
26 | ActiveWorkspace struct {
27 | Id int `json:"id"`
28 | Name string `json:"name"`
29 | } `json:"activeWorkspace"`
30 |
31 | Reserved []int `json:"reserved"`
32 | Scale float64 `json:"scale"`
33 | Transform int `json:"transform"`
34 | Focused bool `json:"focused"`
35 | DpmsStatus bool `json:"dpmsStatus"`
36 | Vrr bool `json:"vrr"`
37 | }
38 |
39 | type Client struct {
40 | Address string `json:"address"`
41 | Mapped bool `json:"mapped"`
42 | Hidden bool `json:"hidden"`
43 | At []int `json:"at"`
44 | Size []int `json:"size"`
45 |
46 | Workspace struct {
47 | Id int `json:"id"`
48 | Name string `json:"name"`
49 | } `json:"workspace"`
50 |
51 | Floating bool `json:"floating"`
52 | Pseudo bool `json:"pseudo"`
53 | Monitor int `json:"monitor"`
54 | Class string `json:"class"`
55 | Title string `json:"title"`
56 | InitialClass string `json:"initialClass"`
57 | InitialTitle string `json:"initialTitle"`
58 | Pid int `json:"pid"`
59 | Xwayland bool `json:"xwayland"`
60 | Pinned bool `json:"pinned"`
61 | Fullscreen int `json:"fullscreen"`
62 | FullscreenClient int `json:"fullscreenClient"`
63 | Grouped []interface{} `json:"grouped"`
64 | Tags []interface{} `json:"tags"`
65 | Swallowing string `json:"swallowing"`
66 | FocusHistoryID int `json:"focusHistoryID"`
67 | InhibitingIdle bool `json:"inhibitingIdle"`
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/ipc/listeners.go:
--------------------------------------------------------------------------------
1 | package ipc
2 |
3 | import (
4 | "strings"
5 | "sync"
6 | )
7 |
8 | type EventListener struct {
9 | Event string
10 | Handler func(string)
11 | ID int
12 | running bool
13 | }
14 |
15 | type eventManager struct {
16 | eventListeners []*EventListener
17 | listenerCounter int
18 | mu sync.Mutex
19 | }
20 |
21 | var (
22 | eventManagerInstance *eventManager
23 | once sync.Once
24 | )
25 |
26 | func getEventManager() *eventManager {
27 | once.Do(func() {
28 | eventManagerInstance = &eventManager{
29 | eventListeners: make([]*EventListener, 0),
30 | listenerCounter: 0,
31 | }
32 | })
33 | return eventManagerInstance
34 | }
35 |
36 | func AddEventListener(event string, handler func(string), running bool) *EventListener {
37 | em := getEventManager()
38 | em.mu.Lock()
39 | defer em.mu.Unlock()
40 |
41 | listener := &EventListener{
42 | Event: event,
43 | Handler: handler,
44 | ID: em.listenerCounter,
45 | running: running,
46 | }
47 |
48 | em.eventListeners = append(em.eventListeners, listener)
49 | em.listenerCounter++
50 | return listener
51 | }
52 |
53 | func DispatchEvent(event string) {
54 | em := getEventManager()
55 | em.mu.Lock()
56 | defer em.mu.Unlock()
57 |
58 | for _, listener := range em.eventListeners {
59 | if strings.Contains(event, listener.Event) && listener.running {
60 | listener.Handler(event)
61 | }
62 | }
63 | }
64 |
65 | func (el *EventListener) Run() {
66 | em := getEventManager()
67 | em.mu.Lock()
68 | defer em.mu.Unlock()
69 |
70 | for i := range em.eventListeners {
71 | if em.eventListeners[i].ID == el.ID {
72 | em.eventListeners[i].running = true
73 | break
74 | }
75 | }
76 | }
77 |
78 | func (el *EventListener) Pause() {
79 | em := getEventManager()
80 | em.mu.Lock()
81 | defer em.mu.Unlock()
82 |
83 | for i := range em.eventListeners {
84 | if em.eventListeners[i].ID == el.ID {
85 | em.eventListeners[i].running = false
86 | break
87 | }
88 | }
89 | }
90 |
91 | func (el *EventListener) IsRunning() bool {
92 | em := getEventManager()
93 | em.mu.Lock()
94 | defer em.mu.Unlock()
95 |
96 | for i := range em.eventListeners {
97 | if em.eventListeners[i].ID == el.ID {
98 | return em.eventListeners[i].running
99 | }
100 | }
101 | return false
102 | }
103 |
104 | func (el *EventListener) Remove() {
105 | em := getEventManager()
106 | em.mu.Lock()
107 | defer em.mu.Unlock()
108 |
109 | for i := range em.eventListeners {
110 | if em.eventListeners[i].ID == el.ID {
111 | em.eventListeners = append(em.eventListeners[:i], em.eventListeners[i+1:]...)
112 | break
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/internal/detectZone/detectZone.go:
--------------------------------------------------------------------------------
1 | package detectzone
2 |
3 | import (
4 | "hypr-dock/internal/state"
5 | "log"
6 |
7 | "github.com/dlasky/gotk3-layershell/layershell"
8 | "github.com/gotk3/gotk3/gdk"
9 | "github.com/gotk3/gotk3/gtk"
10 | )
11 |
12 | func Init(appState *state.State) {
13 | window := appState.GetWindow()
14 |
15 | detectArea, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
16 | if err != nil {
17 | log.Fatal("InitDetectArea(), gtk.WindowNew() | ", err)
18 | }
19 | detectArea.SetName("detect")
20 | detectArea.SetSizeRequest(-1, 1)
21 |
22 | layershell.InitForWindow(detectArea)
23 | layershell.SetNamespace(detectArea, "dock-detect")
24 | layershell.SetLayer(detectArea, layershell.LAYER_SHELL_LAYER_TOP)
25 | selectEdges(detectArea, appState)
26 |
27 | detectArea.Connect("enter-notify-event", func(detectWindow *gtk.Window, e *gdk.Event) {
28 | timer := appState.GetDockHideTimer()
29 | timer.Stop()
30 |
31 | go func() {
32 | layershell.SetLayer(window, layershell.LAYER_SHELL_LAYER_TOP)
33 | }()
34 | })
35 |
36 | detectArea.Connect("leave-notify-event", func(detectWindow *gtk.Window, e *gdk.Event) {
37 | timer := appState.GetDockHideTimer()
38 |
39 | timer.Run(appState.Settings.AutoHideDeley, func() {
40 | layershell.SetLayer(window, layershell.LAYER_SHELL_LAYER_BOTTOM)
41 | })
42 | })
43 |
44 | detectArea.ShowAll()
45 | appState.SetDetectArea(detectArea)
46 | }
47 |
48 | func selectEdges(window *gtk.Window, appState *state.State) {
49 | settings := appState.GetSettings()
50 |
51 | switch settings.Position {
52 | case "left":
53 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_BOTTOM, true)
54 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_LEFT, true)
55 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_TOP, true)
56 | layershell.SetMargin(window, layershell.LAYER_SHELL_EDGE_LEFT, 0)
57 | case "top":
58 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_RIGHT, true)
59 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_LEFT, true)
60 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_TOP, true)
61 | layershell.SetMargin(window, layershell.LAYER_SHELL_EDGE_TOP, 0)
62 | case "right":
63 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_BOTTOM, true)
64 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_RIGHT, true)
65 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_TOP, true)
66 | layershell.SetMargin(window, layershell.LAYER_SHELL_EDGE_RIGHT, 0)
67 | case "bottom":
68 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_BOTTOM, true)
69 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_LEFT, true)
70 | layershell.SetAnchor(window, layershell.LAYER_SHELL_EDGE_RIGHT, true)
71 | layershell.SetMargin(window, layershell.LAYER_SHELL_EDGE_BOTTOM, 0)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/internal/pkg/desktop/parse.go:
--------------------------------------------------------------------------------
1 | package desktop
2 |
3 | import (
4 | "errors"
5 | "hypr-dock/internal/pkg/utils"
6 | "strings"
7 | )
8 |
9 | type Desktop2 map[string]map[string]string
10 |
11 | func NewV2(className string) (*Desktop2, error) {
12 | return Parse(SearchDesktopFile(className))
13 | }
14 |
15 | func Parse(path string) (*Desktop2, error) {
16 | lines, err := utils.LoadTextFile(path)
17 | if err != nil {
18 | return nil, err
19 | }
20 |
21 | result := make(Desktop2)
22 | currentSection := "desktop-entry"
23 |
24 | for _, line := range lines {
25 | line = strings.TrimSpace(line)
26 | if line == "" || strings.HasPrefix(line, "#") {
27 | continue
28 | }
29 |
30 | if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
31 | currentSection = strings.ToLower(line[1 : len(line)-1])
32 | continue
33 | }
34 |
35 | parts := strings.SplitN(line, "=", 2)
36 | if len(parts) != 2 {
37 | continue
38 | }
39 |
40 | key := strings.ToLower(strings.TrimSpace(parts[0]))
41 | value := strings.TrimSpace(parts[1])
42 |
43 | if result[currentSection] == nil {
44 | result[currentSection] = make(map[string]string)
45 | }
46 |
47 | result[currentSection][key] = value
48 | }
49 |
50 | return &result, nil
51 | }
52 |
53 | type Action struct {
54 | Name string
55 | Exec string
56 | Icon string
57 | }
58 |
59 | func GetAppActions(className string) ([]*Action, error) {
60 | appDataPtr, err := Parse(SearchDesktopFile(className))
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | appData := *appDataPtr
66 | general, exist := appData["desktop entry"]
67 | if !exist {
68 | return nil, errors.New("'desktop entry' section not found")
69 | }
70 |
71 | actionsStr, exist := general["actions"]
72 | if !exist {
73 | return nil, errors.New("'actions' field not found in desktop entry")
74 | }
75 |
76 | actionsList := strings.Split(actionsStr, ";")
77 | var actionsRes []*Action
78 |
79 | for _, action := range actionsList {
80 | if action == "" {
81 | continue
82 | }
83 |
84 | key := "desktop action " + action
85 | actionGroup, exist := appData[key]
86 | if !exist {
87 | continue
88 | }
89 |
90 | name, exist := actionGroup["name"]
91 | if !exist {
92 | name = action
93 | }
94 |
95 | exec, exist := actionGroup["exec"]
96 | if !exist {
97 | continue
98 | }
99 |
100 | icon, exist := actionGroup["icon"]
101 | if !exist {
102 | icon = ""
103 | }
104 |
105 | actionsRes = append(actionsRes, &Action{
106 | Name: name,
107 | Exec: exec,
108 | Icon: icon,
109 | })
110 | }
111 |
112 | if len(actionsRes) == 0 {
113 | return nil, errors.New("no valid actions found")
114 | }
115 |
116 | return actionsRes, nil
117 | }
118 |
--------------------------------------------------------------------------------
/internal/pkg/popup/popup.go:
--------------------------------------------------------------------------------
1 | package popup
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/dlasky/gotk3-layershell/layershell"
7 | "github.com/gotk3/gotk3/gtk"
8 | )
9 |
10 | type Popup struct {
11 | x, y int
12 | acitve bool
13 | win *gtk.Window
14 | content gtk.IWidget
15 | xstart, ystart string
16 |
17 | winCallBack func(*gtk.Window) error
18 | }
19 |
20 | func New() *Popup {
21 | return &Popup{
22 | x: 0,
23 | y: 0,
24 | acitve: false,
25 | win: nil,
26 | content: nil,
27 | winCallBack: nil,
28 | }
29 | }
30 |
31 | func (p *Popup) Open(x, y int, xstart, ystart string) error {
32 | p.Close()
33 |
34 | win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | p.win = win
40 | if p.winCallBack != nil {
41 | p.winCallBack(win)
42 | }
43 | initLayerShell(win)
44 |
45 | p.x = x
46 | p.y = y
47 | p.xstart = xstart
48 | p.ystart = ystart
49 |
50 | if p.content != nil {
51 | p.win.Add(p.content)
52 | }
53 | p.setCord()
54 | p.win.ShowAll()
55 | p.acitve = true
56 |
57 | return nil
58 | }
59 |
60 | func (p *Popup) Close() {
61 | p.acitve = false
62 | if p.win != nil {
63 | p.win.Destroy()
64 | p.win = nil
65 | }
66 | }
67 |
68 | func (p *Popup) Set(content gtk.IWidget) error {
69 | if content == nil {
70 | return errors.New("content is nil")
71 | }
72 |
73 | p.content = content
74 | if !p.acitve {
75 | return nil
76 | }
77 |
78 | child, err := p.win.GetChild()
79 | if err == nil && child != nil {
80 | child.ToWidget().Destroy()
81 | }
82 |
83 | p.win.Add(content)
84 | p.win.ShowAll()
85 |
86 | return nil
87 | }
88 |
89 | func (p *Popup) Move(x, y int) {
90 | p.x = x
91 | p.y = y
92 |
93 | if p.acitve {
94 | p.setCord()
95 | }
96 | }
97 |
98 | func (p *Popup) Shift(dx, dy int) {
99 | p.x = p.x + dx
100 | p.y = p.y + dy
101 |
102 | if p.acitve {
103 | p.setCord()
104 | }
105 | }
106 |
107 | func (p *Popup) SetWinCallBack(callback func(*gtk.Window) error) {
108 | p.winCallBack = callback
109 | }
110 |
111 | func (p *Popup) setCord() {
112 | if p.win == nil {
113 | return
114 | }
115 |
116 | xstarts := map[string]layershell.LayerShellEdgeFlags{
117 | "left": layershell.LAYER_SHELL_EDGE_LEFT,
118 | "right": layershell.LAYER_SHELL_EDGE_RIGHT,
119 | }
120 |
121 | ystarts := map[string]layershell.LayerShellEdgeFlags{
122 | "top": layershell.LAYER_SHELL_EDGE_TOP,
123 | "bottom": layershell.LAYER_SHELL_EDGE_BOTTOM,
124 | }
125 |
126 | layershell.SetAnchor(p.win, xstarts[p.xstart], true)
127 | layershell.SetAnchor(p.win, ystarts[p.ystart], true)
128 | layershell.SetMargin(p.win, xstarts[p.xstart], p.x)
129 | layershell.SetMargin(p.win, ystarts[p.ystart], p.y)
130 | }
131 |
132 | func initLayerShell(win *gtk.Window) {
133 | layershell.InitForWindow(win)
134 | layershell.SetNamespace(win, "dock-popup")
135 | layershell.SetLayer(win, layershell.LAYER_SHELL_LAYER_TOP)
136 | layershell.SetExclusiveZone(win, -1)
137 | }
138 |
--------------------------------------------------------------------------------
/internal/pkg/utils/gtk.utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "log"
5 | "math"
6 | "strings"
7 |
8 | "github.com/gotk3/gotk3/gdk"
9 | "github.com/gotk3/gotk3/gtk"
10 | "github.com/pkg/errors"
11 | )
12 |
13 | func CreateImageWidthScale(source string, size int, scaleFactor float64) (*gtk.Image, error) {
14 | scaleSize := int(math.Round(float64(size) * math.Max(scaleFactor, 0)))
15 |
16 | return CreateImage(source, scaleSize)
17 | }
18 |
19 | func CreateImage(source string, size int) (*gtk.Image, error) {
20 | // Create image in file
21 | if strings.Contains(source, "/") {
22 | pixbuf, err := gdk.PixbufNewFromFileAtSize(source, size, size)
23 | if err != nil {
24 | log.Println(err)
25 | return CreateImage("image-missing", size)
26 | }
27 |
28 | return CreateImageFromPixbuf(pixbuf), nil
29 | }
30 |
31 | // Create image in icon name
32 | iconTheme, err := gtk.IconThemeGetDefault()
33 | if err != nil {
34 | log.Println("Unable to icon theme:", err)
35 | return CreateImage("image-missing", size)
36 | }
37 |
38 | pixbuf, err := iconTheme.LoadIcon(source, size, gtk.ICON_LOOKUP_FORCE_SIZE)
39 | if err != nil {
40 | log.Println(source, err)
41 | return CreateImage("image-missing", size)
42 | }
43 |
44 | return CreateImageFromPixbuf(pixbuf), nil
45 | }
46 |
47 | func CreateImageFromPixbuf(pixbuf *gdk.Pixbuf) *gtk.Image {
48 | image, err := gtk.ImageNewFromPixbuf(pixbuf)
49 | if err != nil {
50 | log.Println("Error creating image from pixbuf:", err)
51 | return nil
52 | }
53 | return image
54 | }
55 |
56 | func AddStyle(widget gtk.IWidget, style string) (*gtk.CssProvider, error) {
57 | provider, err := gtk.CssProviderNew()
58 | if err != nil {
59 | return nil, err
60 | }
61 |
62 | err = provider.LoadFromData(style)
63 | if err != nil {
64 | return nil, err
65 | }
66 |
67 | context, err := widget.ToWidget().GetStyleContext()
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | context.AddProvider(provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
73 |
74 | return provider, nil
75 | }
76 |
77 | func AddCssProvider(cssFile string) error {
78 | cssProvider, err := gtk.CssProviderNew()
79 | if err != nil {
80 | log.Printf("Failed to create CSS provider: %v", err)
81 | return errors.Wrap(err, "failed to create CSS provider")
82 | }
83 |
84 | if err := cssProvider.LoadFromPath(cssFile); err != nil {
85 | log.Printf("Failed to load CSS from %q: %v", cssFile, err)
86 | return errors.Wrapf(err, "failed to load CSS from %q", cssFile)
87 | }
88 |
89 | screen, err := gdk.ScreenGetDefault()
90 | if err != nil {
91 | log.Printf("Failed to get default screen: %v", err)
92 | return errors.Wrap(err, "failed to get default screen")
93 | }
94 |
95 | gtk.AddProviderForScreen(
96 | screen, cssProvider,
97 | gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
98 | )
99 |
100 | return nil
101 | }
102 |
103 | func RemoveStyleProvider(widget *gtk.Box, provider *gtk.CssProvider) {
104 | if provider == nil {
105 | log.Println("provider is nil")
106 | return
107 | }
108 |
109 | styleContext, err := widget.GetStyleContext()
110 | if err != nil {
111 | log.Println(err)
112 | return
113 | }
114 |
115 | styleContext.RemoveProvider(provider)
116 | }
117 |
--------------------------------------------------------------------------------
/internal/pkg/desktop/desktop.go:
--------------------------------------------------------------------------------
1 | package desktop
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "slices"
7 | "strings"
8 | )
9 |
10 | type Desktop struct {
11 | Name string
12 | Icon string
13 | Exec string
14 | SingleWindow bool
15 | }
16 |
17 | // var desktopDirs = GetAppDirs()
18 |
19 | func New(className string) *Desktop {
20 | errData := &Desktop{
21 | Name: "Untitle",
22 | Icon: "",
23 | Exec: "",
24 | SingleWindow: false,
25 | }
26 |
27 | allData, err := Parse(SearchDesktopFile(className))
28 | if err != nil {
29 | return errData
30 | }
31 |
32 | appData := *allData
33 | general, exist := appData["desktop entry"]
34 | if !exist {
35 | return errData
36 | }
37 |
38 | name, exist := general["name"]
39 | if !exist {
40 | name = errData.Name
41 | }
42 |
43 | icon, exist := general["icon"]
44 | if !exist {
45 | icon = ""
46 | }
47 |
48 | exec, exist := general["exec"]
49 | if !exist {
50 | exec = ""
51 | }
52 |
53 | singleWindowStr, exist := general["singlemainwindow"]
54 | if !exist {
55 | singleWindowStr = "false"
56 | }
57 |
58 | singleWindow := singleWindowStr == "true"
59 |
60 | return &Desktop{
61 | Name: name,
62 | Icon: icon,
63 | Exec: exec,
64 | SingleWindow: singleWindow,
65 | }
66 | }
67 |
68 | func SearchDesktopFile(className string) string {
69 | for _, appDir := range GetAppDirs() {
70 | desktopFile := className + ".desktop"
71 | _, err := os.Stat(filepath.Join(appDir, desktopFile))
72 | if err == nil {
73 | return filepath.Join(appDir, desktopFile)
74 | }
75 |
76 | // If file non found
77 | files, _ := os.ReadDir(appDir)
78 | for _, file := range files {
79 | fileName := file.Name()
80 |
81 | // "krita" > "org.kde.krita.desktop" / "lutris" > "net.lutris.Lutris.desktop"
82 | if strings.Count(fileName, ".") > 1 && strings.Contains(fileName, className) {
83 | return filepath.Join(appDir, fileName)
84 | }
85 | // "VirtualBox Manager" > "virtualbox.desktop"
86 | if fileName == strings.Split(strings.ToLower(className), " ")[0]+".desktop" {
87 | return filepath.Join(appDir, fileName)
88 | }
89 | }
90 | }
91 |
92 | return ""
93 | }
94 |
95 | func GetAppDirs() []string {
96 | var dirs []string
97 | xdgDataDirs := ""
98 |
99 | home := os.Getenv("HOME")
100 | xdgDataHome := os.Getenv("XDG_DATA_HOME")
101 | if os.Getenv("XDG_DATA_DIRS") != "" {
102 | xdgDataDirs = os.Getenv("XDG_DATA_DIRS")
103 | } else {
104 | xdgDataDirs = "/usr/local/share/:/usr/share/"
105 | }
106 | if xdgDataHome != "" {
107 | dirs = append(dirs, filepath.Join(xdgDataHome, "applications"))
108 | } else if home != "" {
109 | dirs = append(dirs, filepath.Join(home, ".local/share/applications"))
110 | }
111 | for _, d := range strings.Split(xdgDataDirs, ":") {
112 | dirs = append(dirs, filepath.Join(d, "applications"))
113 | }
114 | flatpakDirs := []string{filepath.Join(home, ".local/share/flatpak/exports/share/applications"),
115 | "/var/lib/flatpak/exports/share/applications"}
116 |
117 | for _, d := range flatpakDirs {
118 | if !slices.Contains(dirs, d) {
119 | dirs = append(dirs, d)
120 | }
121 | }
122 | return dirs
123 | }
124 |
--------------------------------------------------------------------------------
/internal/pkg/indicator/indicator.go:
--------------------------------------------------------------------------------
1 | package indicator
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "hypr-dock/internal/pkg/utils"
7 | "hypr-dock/internal/settings"
8 | "os"
9 | "path/filepath"
10 | "slices"
11 | "sort"
12 | "strconv"
13 | "strings"
14 |
15 | "github.com/gotk3/gotk3/gtk"
16 | )
17 |
18 | // IndicatorFile represents an indicator image file
19 | type IndicatorFile struct {
20 | Number int // Numeric prefix of the file (e.g. 0 from "0.svg")
21 | Extension string // File extension including dot (e.g. ".svg")
22 | FullName string // Complete filename (e.g. "0.svg")
23 | }
24 |
25 | // New creates a new indicator image based on instances count
26 | func New(instances int, settings settings.Settings) (*gtk.Image, error) {
27 | available, err := GetAvailable(settings)
28 | if err != nil {
29 | return nil, fmt.Errorf("failed to get indicators: %w", err)
30 | }
31 | if len(available) < 2 {
32 | return nil, errors.New("at least 2 indicator files required (0.* and *.*)")
33 | }
34 | if available[0].Number != 0 {
35 | return nil, errors.New("at least 2 indicator files required (0.* and *.*)")
36 | }
37 |
38 | selected := selectIndicatorFile(instances, available)
39 | path := filepath.Join(settings.CurrentThemeDir, "point", selected.FullName)
40 | return utils.CreateImageWidthScale(path, settings.IconSize, 0.56)
41 | }
42 |
43 | // selectIndicatorFile chooses the appropriate indicator file based on instances count
44 | func selectIndicatorFile(instances int, files []IndicatorFile) IndicatorFile {
45 | selected := files[0]
46 | for _, file := range files {
47 | if file.Number > instances {
48 | break
49 | }
50 | selected = file
51 | }
52 | return selected
53 | }
54 |
55 | // GetAvailable returns all valid indicator files sorted by their numeric value
56 | func GetAvailable(settings settings.Settings) ([]IndicatorFile, error) {
57 | dirPath := filepath.Join(settings.CurrentThemeDir, "point")
58 |
59 | if _, err := os.Stat(dirPath); os.IsNotExist(err) {
60 | return nil, fmt.Errorf("indicator directory not found: %w", err)
61 | }
62 |
63 | entries, err := os.ReadDir(dirPath)
64 | if err != nil {
65 | return nil, fmt.Errorf("failed to read indicator directory: %w", err)
66 | }
67 |
68 | var files []IndicatorFile
69 | for _, entry := range entries {
70 | if entry.IsDir() {
71 | continue
72 | }
73 |
74 | file, ok := parseIndicatorFile(entry.Name())
75 | if ok {
76 | files = append(files, file)
77 | }
78 | }
79 |
80 | sort.Slice(files, func(i, j int) bool {
81 | return files[i].Number < files[j].Number
82 | })
83 |
84 | return files, nil
85 | }
86 |
87 | // parseIndicatorFile attempts to parse a filename into an IndicatorFile
88 | func parseIndicatorFile(name string) (IndicatorFile, bool) {
89 | ext := strings.ToLower(filepath.Ext(name))
90 | if !isSupportedExtension(ext) {
91 | return IndicatorFile{}, false
92 | }
93 |
94 | baseName := name[:len(name)-len(ext)]
95 | num, err := strconv.Atoi(baseName)
96 | if err != nil {
97 | return IndicatorFile{}, false
98 | }
99 |
100 | return IndicatorFile{
101 | Number: num,
102 | Extension: ext,
103 | FullName: name,
104 | }, true
105 | }
106 |
107 | // isSupportedExtension checks if the extension is valid for indicator files
108 | func isSupportedExtension(ext string) bool {
109 | supported := []string{".svg", ".jpg", ".png", ".webp"}
110 | return slices.Contains(supported, ext)
111 | }
112 |
--------------------------------------------------------------------------------
/internal/btnctl/utils.go:
--------------------------------------------------------------------------------
1 | package btnctl
2 |
3 | import (
4 | "hypr-dock/internal/item"
5 | "hypr-dock/internal/layering"
6 | "hypr-dock/internal/state"
7 | "hypr-dock/pkg/ipc"
8 | "log"
9 |
10 | "github.com/gotk3/gotk3/gdk"
11 | "github.com/gotk3/gotk3/gtk"
12 | )
13 |
14 | func connectContextMenu(item *item.Item, appState *state.State) {
15 | settings := appState.GetSettings()
16 |
17 | item.Button.Connect("button-release-event", func(button *gtk.Button, e *gdk.Event) {
18 | event := gdk.EventButtonNewFromEvent(e)
19 | if event.Button() == 3 {
20 | menu, err := item.ContextMenu(settings)
21 | if err != nil {
22 | log.Println(err)
23 | return
24 | }
25 |
26 | win, zone, err := getActivateZone(item.Button, settings.ContextPos, settings.Position)
27 | if err != nil {
28 | log.Println(err)
29 | return
30 | }
31 |
32 | firstg, secondg := getGravity(settings.Position)
33 | menu.PopupAtRect(win, zone, firstg, secondg, nil)
34 | ipc.DispatchEvent("hd>>open-context")
35 | menu.Connect("deactivate", func() {
36 | ipc.DispatchEvent("hd>>close-context")
37 | dispather(appState, item.Button)
38 | })
39 |
40 | return
41 | }
42 | })
43 | }
44 |
45 | func leftClick(btn *gtk.Button, handler func(e *gdk.Event)) {
46 | btn.Connect("button-release-event", func(button *gtk.Button, e *gdk.Event) {
47 | event := gdk.EventButtonNewFromEvent(e)
48 | if event.Button() != 3 {
49 | handler(e)
50 | }
51 | })
52 | }
53 |
54 | func dispather(appState *state.State, btn *gtk.Button) {
55 | window := appState.GetWindow()
56 | btn.SetStateFlags(gtk.STATE_FLAG_NORMAL, true)
57 | if appState.GetSettings().Layer == "auto" {
58 | layering.DispathLeaveEvent(window, nil, appState)
59 | }
60 | }
61 |
62 | func getActivateZone(v *gtk.Button, margin int, pos string) (*gdk.Window, *gdk.Rectangle, error) {
63 | var rect *gdk.Rectangle
64 |
65 | win, err := v.GetWindow()
66 | if err != nil {
67 | return nil, nil, err
68 | }
69 |
70 | switch pos {
71 | case "bottom":
72 | rect = gdk.RectangleNew(
73 | v.GetAllocation().GetX(),
74 | 0-margin,
75 | v.GetAllocatedWidth(),
76 | v.GetAllocatedHeight(),
77 | )
78 | case "left":
79 | rect = gdk.RectangleNew(
80 | 0-(v.GetAllocatedWidth()/2)-v.GetAllocation().GetX()+win.WindowGetWidth()+margin,
81 | v.GetAllocation().GetY(),
82 | v.GetAllocatedWidth(),
83 | v.GetAllocatedHeight(),
84 | )
85 | case "top":
86 | rect = gdk.RectangleNew(
87 | v.GetAllocation().GetX(),
88 | 0-(v.GetAllocatedHeight()/2)-v.GetAllocation().GetY()+win.WindowGetHeight()+margin,
89 | v.GetAllocatedWidth(),
90 | v.GetAllocatedHeight(),
91 | )
92 | case "right":
93 | rect = gdk.RectangleNew(
94 | 0-margin,
95 | v.GetAllocation().GetY(),
96 | v.GetAllocatedWidth(),
97 | v.GetAllocatedHeight(),
98 | )
99 | }
100 |
101 | return win, rect, err
102 | }
103 |
104 | func getGravity(pos string) (gdk.Gravity, gdk.Gravity) {
105 | var first, second gdk.Gravity
106 |
107 | switch pos {
108 | case "bottom":
109 | first = gdk.GDK_GRAVITY_NORTH
110 | second = gdk.GDK_GRAVITY_SOUTH
111 | case "left":
112 | first = gdk.GDK_GRAVITY_EAST
113 | second = gdk.GDK_GRAVITY_WEST
114 | case "top":
115 | second = gdk.GDK_GRAVITY_NORTH
116 | first = gdk.GDK_GRAVITY_SOUTH
117 | case "right":
118 | second = gdk.GDK_GRAVITY_EAST
119 | first = gdk.GDK_GRAVITY_WEST
120 | }
121 |
122 | return first, second
123 | }
124 |
--------------------------------------------------------------------------------
/internal/btnctl/btnctl.go:
--------------------------------------------------------------------------------
1 | package btnctl
2 |
3 | import (
4 | "hypr-dock/internal/item"
5 | "hypr-dock/internal/placeholders"
6 | "hypr-dock/internal/state"
7 | "hypr-dock/pkg/ipc"
8 | "log"
9 |
10 | "github.com/gotk3/gotk3/gdk"
11 | "github.com/gotk3/gotk3/glib"
12 | )
13 |
14 | func Dispatch(item *item.Item, appState *state.State) {
15 | connectContextMenu(item, appState)
16 |
17 | if appState.GetSettings().Preview == "none" {
18 | defaultControl(item, appState)
19 | return
20 | }
21 |
22 | previewControl(item, appState)
23 | }
24 |
25 | func previewControl(item *item.Item, appState *state.State) {
26 | settings := appState.GetSettings()
27 | pv := appState.GetPV()
28 | showTimer := pv.GetShowTimer()
29 | hideTimer := pv.GetHideTimer()
30 | moveTimer := pv.GetMoveTimer()
31 |
32 | show := func() {
33 | glib.IdleAdd(func() {
34 | pv.Show(item, settings)
35 | })
36 | pv.SetActive(true)
37 | }
38 |
39 | hide := func() {
40 | glib.IdleAdd(func() {
41 | pv.Hide()
42 | })
43 | pv.SetActive(false)
44 | }
45 |
46 | move := func() {
47 | glib.IdleAdd(func() {
48 | pv.Change(item, settings)
49 | })
50 | }
51 |
52 | ipc.AddEventListener("hd>>open-context", func(e string) {
53 | showTimer.Stop()
54 | if pv.GetActive() {
55 | hideTimer.Run(0, hide)
56 | }
57 | }, true)
58 |
59 | leftClick(item.Button, func(e *gdk.Event) {
60 | if item.Instances == 0 {
61 | placeholders.Run(item.DesktopData.Exec)
62 | }
63 | if item.Instances == 1 {
64 | ipc.Hyprctl("dispatch focuswindow address:" + item.Windows[0]["Address"])
65 | ipc.DispatchEvent("hd>>focus-window")
66 | }
67 | if item.Instances > 1 {
68 | if !pv.GetActive() {
69 | showTimer.Run(0, show)
70 | pv.SetCurrentClass(item.ClassName)
71 | }
72 | }
73 | })
74 |
75 | item.Button.Connect("enter-notify-event", func() {
76 | if item.Instances == 0 {
77 | return
78 | }
79 |
80 | hideTimer.Stop()
81 |
82 | if pv.GetActive() && pv.HasClassChanged(item.ClassName) {
83 | // fmt.Println("if true")
84 | moveTimer.Stop()
85 | moveTimer.Run(settings.PreviewAdvanced.MoveDelay, move)
86 | pv.SetCurrentClass(item.ClassName)
87 | return
88 | }
89 |
90 | if !pv.GetActive() {
91 | showTimer.Run(settings.PreviewAdvanced.ShowDelay, show)
92 | pv.SetCurrentClass(item.ClassName)
93 | }
94 | })
95 |
96 | item.Button.Connect("leave-notify-event", func() {
97 | if item.Instances == 0 {
98 | return
99 | }
100 |
101 | showTimer.Stop()
102 | if pv.GetActive() {
103 | hideTimer.Run(settings.PreviewAdvanced.HideDelay, hide)
104 | }
105 | })
106 | }
107 |
108 | func defaultControl(item *item.Item, appState *state.State) {
109 | settings := appState.GetSettings()
110 |
111 | leftClick(item.Button, func(e *gdk.Event) {
112 | if item.Instances == 0 {
113 | placeholders.Run(item.DesktopData.Exec)
114 | }
115 | if item.Instances == 1 {
116 | ipc.Hyprctl("dispatch focuswindow address:" + item.Windows[0]["Address"])
117 | }
118 | if item.Instances > 1 {
119 | menu, err := item.WindowsMenu()
120 | if err != nil {
121 | log.Println(err)
122 | return
123 | }
124 |
125 | win, zone, err := getActivateZone(item.Button, settings.ContextPos, settings.Position)
126 | if err != nil {
127 | log.Println(err)
128 | return
129 | }
130 |
131 | firstg, secondg := getGravity(settings.Position)
132 | menu.PopupAtRect(win, zone, firstg, secondg, nil)
133 | menu.Connect("deactivate", func() {
134 | dispather(appState, item.Button)
135 | })
136 | }
137 | })
138 | }
139 |
--------------------------------------------------------------------------------
/internal/settings/settings.go:
--------------------------------------------------------------------------------
1 | package settings
2 |
3 | import (
4 | "hypr-dock/internal/pkg/cfg"
5 | "hypr-dock/internal/pkg/flags"
6 | "hypr-dock/internal/pkg/utils"
7 | "hypr-dock/pkg/ipc"
8 | "log"
9 | "os"
10 | "path/filepath"
11 | "strings"
12 | )
13 |
14 | type Settings struct {
15 | cfg.Config
16 | ConfigDir string
17 | ConfigPath string
18 | PinnedPath string
19 | ThemesDir string
20 | CurrentThemeDir string
21 | CurrentThemeConfigPath string
22 | CurrentThemeStylePath string
23 | PinnedApps []string
24 | }
25 |
26 | func Init() (Settings, error) {
27 | const DefaultTheme = "lotos"
28 |
29 | var settings Settings
30 |
31 | flags := flags.Get(DefaultTheme)
32 |
33 | if flags.DevMode {
34 | settings.ConfigDir = setConfigDir("dev")
35 | } else {
36 | settings.ConfigDir = setConfigDir("normal")
37 | }
38 |
39 | settings.PinnedPath = filepath.Join(settings.ConfigDir, "pinned.json")
40 | settings.PinnedApps = cfg.ReadItemList(settings.PinnedPath)
41 | defaultConfigPath := filepath.Join(settings.ConfigDir, "config.jsonc")
42 |
43 | if flags.Config == "~/.config/hypr-dock" {
44 | settings.ConfigPath = defaultConfigPath
45 | } else {
46 | settings.ConfigPath = expandPath(flags.Config)
47 | }
48 |
49 | settings.Config = cfg.ReadConfig(settings.ConfigPath, settings.ThemesDir)
50 |
51 | settings.ThemesDir = filepath.Join(settings.ConfigDir, "themes")
52 | settings.CurrentThemeDir = filepath.Join(settings.ThemesDir, settings.CurrentTheme)
53 |
54 | if !utils.FileExists(settings.CurrentThemeDir) {
55 | log.Println("Current theme not found (", settings.CurrentTheme, "). Loading default theme")
56 |
57 | if settings.CurrentTheme == DefaultTheme {
58 | log.Println("Default theme not found")
59 | }
60 |
61 | settings.CurrentTheme = DefaultTheme
62 | }
63 |
64 | settings.CurrentThemeStylePath = filepath.Join(settings.CurrentThemeDir, "style.css")
65 | settings.CurrentThemeConfigPath = filepath.Join(settings.CurrentThemeDir, settings.CurrentTheme+".jsonc")
66 |
67 | themeConfig := cfg.ReadTheme(settings.CurrentThemeConfigPath, settings.Config)
68 | if themeConfig != nil {
69 | settings.Blur = themeConfig.Blur
70 | settings.Spacing = themeConfig.Spacing
71 | settings.PreviewStyle = themeConfig.PreviewStyle
72 | }
73 |
74 | if settings.Blur == "true" {
75 | enableBlur()
76 | ipc.AddEventListener("configreloaded", func(event string) {
77 | go enableBlur()
78 | }, true)
79 | }
80 |
81 | return settings, nil
82 | }
83 |
84 | func setConfigDir(mode string) string {
85 | homeDir, err := os.UserHomeDir()
86 | if err != nil {
87 | log.Fatal("Home dir: " + err.Error())
88 | }
89 |
90 | exePath, err := os.Executable()
91 | if err != nil {
92 | log.Fatal("Exe dir: " + err.Error())
93 | }
94 |
95 | exeDir := filepath.Dir(exePath)
96 |
97 | runModes := map[string]func() string{
98 | "normal": func() string {
99 | return filepath.Join(homeDir, ".config/hypr-dock")
100 | },
101 | "dev": func() string {
102 | return filepath.Join(filepath.Dir(exeDir), "configs")
103 | },
104 | }
105 | return runModes[mode]()
106 | }
107 |
108 | func expandPath(path string) string {
109 | if strings.HasPrefix(path, "~/") {
110 | home, _ := os.UserHomeDir()
111 | return filepath.Join(home, path[2:])
112 | }
113 | return path
114 | }
115 |
116 | func enableBlur() {
117 | ipc.Hyprctl("keyword layerrule blur,hypr-dock")
118 | ipc.Hyprctl("keyword layerrule ignorealpha 0.1,hypr-dock")
119 |
120 | ipc.Hyprctl("keyword layerrule blur,dock-popup")
121 | ipc.Hyprctl("keyword layerrule ignorealpha 0.1,dock-popup")
122 | }
123 |
--------------------------------------------------------------------------------
/docs/tasks/ipc-main.md:
--------------------------------------------------------------------------------
1 | ### **Техническое задание: Реализация `hypr-dock-ctl` для управления hypr-dock**
2 |
3 | #### **1. Цель**
4 | Создать **отдельный CLI-бинарник** `hypr-dock-ctl` для управления доком через IPC, обеспечив:
5 | - **Смену режима слоя** (`auto` ↔ `exclusive`) без перезапуска дока.
6 | - **Единообразие с `hyprctl`** (синтаксис, флаги, вывод).
7 | - **Масштабируемость** для будущих команд (темы, настройки, статус).
8 |
9 | ---
10 |
11 | ### **2. Требования**
12 |
13 | #### **2.1. Базовый функционал (v1.0)**
14 | - **Команда `toggle`**
15 | ```bash
16 | hypr-dock-ctl toggle layer # Переключает между auto и exclusive
17 | ```
18 | - Отправляет команду через **Unix-socket** (`/tmp/hypr-dock.sock`).
19 | - Если док **не запущен** — игнорирует команду (или выводит ошибку).
20 |
21 | - **Команда `get`**
22 | ```bash
23 | hypr-dock-ctl get layer # Выводит текущий режим (auto/exclusive)
24 | ```
25 | - Возвращает текст или JSON (если `--json`).
26 |
27 | - **Интеграция с Hyprland**
28 | ```bash
29 | bind = SuperShift, D, exec, hypr-dock-ctl toggle layer
30 | ```
31 |
32 | #### **2.2. Дополнительные требования**
33 | - **Единый стиль с `hyprctl`**:
34 | - Поддержка флагов `--json`, `--help`.
35 | - Чёткие сообщения об ошибках (например, `Dock is not running`).
36 | - **Только один инстанс дока**:
37 | - При запуске `hypr-dock` проверяется, что сокет свободен.
38 |
39 | ---
40 |
41 | ### **3. Концепция реализации**
42 |
43 | #### **3.1. IPC-протокол**
44 | - **Формат команд**: Текстовые строки (для простоты).
45 | ```text
46 | TOGGLE_LAYER
47 | GET_LAYER
48 | ```
49 | - **Ответы**:
50 | - `OK` — успех.
51 | - `ERROR: Not running` — док не активен.
52 | - `auto`/`exclusive` — для `get`.
53 |
54 | #### **3.2. Схема взаимодействия**
55 | ```mermaid
56 | sequenceDiagram
57 | participant Hyprland
58 | participant hypr-dock-ctl
59 | participant hypr-dock (сервер)
60 |
61 | Hyprland ->> hypr-dock-ctl: toggle layer
62 | hypr-dock-ctl ->> hypr-dock: TOGGLE_LAYER (сокет)
63 | hypr-dock -->> hypr-dock-ctl: OK
64 | hypr-dock-ctl -->> Hyprland: Успех (код 0)
65 | ```
66 |
67 | #### **3.3. Обработка ошибок**
68 | | Ситуация | Действие |
69 | |---------------------------|-----------------------------------|
70 | | Док не запущен | `ERROR: Dock is not running` |
71 | | Неверная команда | `ERROR: Unknown command` |
72 | | Сокет занят/недоступен | `ERROR: Cannot connect to dock` |
73 |
74 | ---
75 |
76 | ### **4. Возможные расширения (будущие версии)**
77 |
78 | #### **4.1. Управление темами**
79 | ```bash
80 | hypr-dock-ctl set theme dark
81 | hypr-dock-ctl get theme
82 | ```
83 |
84 | #### **4.2. Динамические настройки**
85 | ```bash
86 | hypr-dock-ctl set margin 10
87 | hypr-dock-ctl set spacing 5
88 | ```
89 |
90 | #### **4.3. Статус и отладка**
91 | ```bash
92 | hypr-dock-ctl status # Вывод всех параметров (JSON)
93 | hypr-dock-ctl reload # Перезагрузка конфига
94 | ```
95 |
96 | #### **4.4. Интеграция с `hyprctl`**
97 | В будущем — добавление подкоманд в `hyprctl`:
98 | ```bash
99 | hyprctl dock toggle layer
100 | hyprctl dock get theme
101 | ```
102 |
103 | ---
104 | ### **5. Этапы реализации**
105 | 1. **Реализация IPC-сервера** в `hypr-dock` (обработка команд).
106 | 2. **Создание `hypr-dock-ctl`** с командами `toggle` и `get`.
107 | 3. **Тестирование**:
108 | - Запуск/остановка дока.
109 | - Конкуренция за сокет.
110 | 4. **Документация**:
111 | - `man hypr-dock-ctl`.
112 | - Примеры для `hyprland.conf`.
--------------------------------------------------------------------------------
/docs/tasks/ipc/ipc-main.md:
--------------------------------------------------------------------------------
1 | ### **Техническое задание: Реализация `hypr-dock-ctl` для управления hypr-dock**
2 |
3 | #### **1. Цель**
4 | Создать **отдельный CLI-бинарник** `hypr-dock-ctl` для управления доком через IPC, обеспечив:
5 | - **Смену режима слоя** (`auto` ↔ `exclusive`) без перезапуска дока.
6 | - **Единообразие с `hyprctl`** (синтаксис, флаги, вывод).
7 | - **Масштабируемость** для будущих команд (темы, настройки, статус).
8 |
9 | ---
10 |
11 | ### **2. Требования**
12 |
13 | #### **2.1. Базовый функционал (v1.0)**
14 | - **Команда `toggle`**
15 | ```bash
16 | hypr-dock-ctl toggle layer # Переключает между auto и exclusive
17 | ```
18 | - Отправляет команду через **Unix-socket** (`/tmp/hypr-dock.sock`).
19 | - Если док **не запущен** — игнорирует команду (или выводит ошибку).
20 |
21 | - **Команда `get`**
22 | ```bash
23 | hypr-dock-ctl get layer # Выводит текущий режим (auto/exclusive)
24 | ```
25 | - Возвращает текст или JSON (если `--json`).
26 |
27 | - **Интеграция с Hyprland**
28 | ```bash
29 | bind = SuperShift, D, exec, hypr-dock-ctl toggle layer
30 | ```
31 |
32 | #### **2.2. Дополнительные требования**
33 | - **Единый стиль с `hyprctl`**:
34 | - Поддержка флагов `--json`, `--help`.
35 | - Чёткие сообщения об ошибках (например, `Dock is not running`).
36 | - **Только один инстанс дока**:
37 | - При запуске `hypr-dock` проверяется, что сокет свободен.
38 |
39 | ---
40 |
41 | ### **3. Концепция реализации**
42 |
43 | #### **3.1. IPC-протокол**
44 | - **Формат команд**: Текстовые строки (для простоты).
45 | ```text
46 | TOGGLE_LAYER
47 | GET_LAYER
48 | ```
49 | - **Ответы**:
50 | - `OK` — успех.
51 | - `ERROR: Not running` — док не активен.
52 | - `auto`/`exclusive` — для `get`.
53 |
54 | #### **3.2. Схема взаимодействия**
55 | ```mermaid
56 | sequenceDiagram
57 | participant Hyprland
58 | participant hypr-dock-ctl
59 | participant hypr-dock (сервер)
60 |
61 | Hyprland ->> hypr-dock-ctl: toggle layer
62 | hypr-dock-ctl ->> hypr-dock: TOGGLE_LAYER (сокет)
63 | hypr-dock -->> hypr-dock-ctl: OK
64 | hypr-dock-ctl -->> Hyprland: Успех (код 0)
65 | ```
66 |
67 | #### **3.3. Обработка ошибок**
68 | | Ситуация | Действие |
69 | |---------------------------|-----------------------------------|
70 | | Док не запущен | `ERROR: Dock is not running` |
71 | | Неверная команда | `ERROR: Unknown command` |
72 | | Сокет занят/недоступен | `ERROR: Cannot connect to dock` |
73 |
74 | ---
75 |
76 | ### **4. Возможные расширения (будущие версии)**
77 |
78 | #### **4.1. Управление темами**
79 | ```bash
80 | hypr-dock-ctl set theme dark
81 | hypr-dock-ctl get theme
82 | ```
83 |
84 | #### **4.2. Динамические настройки**
85 | ```bash
86 | hypr-dock-ctl set margin 10
87 | hypr-dock-ctl set spacing 5
88 | ```
89 |
90 | #### **4.3. Статус и отладка**
91 | ```bash
92 | hypr-dock-ctl status # Вывод всех параметров (JSON)
93 | hypr-dock-ctl reload # Перезагрузка конфига
94 | ```
95 |
96 | #### **4.4. Интеграция с `hyprctl`**
97 | В будущем — добавление подкоманд в `hyprctl`:
98 | ```bash
99 | hyprctl dock toggle layer
100 | hyprctl dock get theme
101 | ```
102 |
103 | ---
104 | ### **5. Этапы реализации**
105 | 1. **Реализация IPC-сервера** в `hypr-dock` (обработка команд).
106 | 2. **Создание `hypr-dock-ctl`** с командами `toggle` и `get`.
107 | 3. **Тестирование**:
108 | - Запуск/остановка дока.
109 | - Конкуренция за сокет.
110 | 4. **Документация**:
111 | - `man hypr-dock-ctl`.
112 | - Примеры для `hyprland.conf`.
113 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37 h1:28uU3TtuvQ6KRndxg9TrC868jBWmSKgh0GTXkACCXmA=
2 | github.com/allan-simon/go-singleinstance v0.0.0-20210120080615-d0997106ab37/go.mod h1:6AXRstqK+32jeFmw89QGL2748+dj34Av4xc/I9oo9BY=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774 h1:o87OVL4olQBlVwN3+NSVQpS6gj9FWUYtxOfHXWZigUE=
7 | github.com/dlasky/gotk3-layershell v0.0.0-20240515133811-5c5115f0d774/go.mod h1:JHLx2Wz4mAPVwn4PFhC69ydwyHP4A3wQvlg7HKVVc1U=
8 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
9 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
10 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
11 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
12 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
13 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
14 | github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
15 | github.com/gotk3/gotk3 v0.6.3 h1:+Ke4WkM1TQUNOlM2TZH6szqknqo+zNbX3BZWVXjSHYw=
16 | github.com/gotk3/gotk3 v0.6.3/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
17 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
18 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
19 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
20 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
21 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
22 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
23 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
24 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
25 | github.com/pdf/go-wayland v0.0.2 h1:/RzSdcUN3kSgd9Dyd8j4xFDwtJpYSprXSPxJ9AWDdlI=
26 | github.com/pdf/go-wayland v0.0.2/go.mod h1:aJPz7VZFFutb5bx9UwD6Pzw4gYziTBf2tRZsgaKx5xA=
27 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
28 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
32 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
33 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
34 | github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a h1:a6TNDN9CgG+cYjaeN8l2mc4kSz2iMiCDQxPEyltUV/I=
35 | github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
36 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
37 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
38 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
39 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
40 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
42 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
44 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
45 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
46 |
--------------------------------------------------------------------------------
/docs/tasks/ipc/dockIPC-example.md:
--------------------------------------------------------------------------------
1 | # **Модуль dockIPC**
2 |
3 | Реализация модуля для работы с IPC через Unix domain sockets:
4 |
5 | ```go
6 | package dockIPC
7 |
8 | import (
9 | "errors"
10 | "net"
11 | "os"
12 | "syscall"
13 | )
14 |
15 | // StartServer - запускает IPC сервер
16 | // fileName: путь к сокету (например "/tmp/hypr-dock.sock")
17 | // handler: функция обработки входящих команд
18 | func StartServer(fileName string, handler func(string) ([]byte, error)) error {
19 | // Удаляем старый сокет если существует
20 | if err := os.RemoveAll(fileName); err != nil {
21 | return err
22 | }
23 |
24 | // Создаем Unix domain socket
25 | listener, err := net.Listen("unix", fileName)
26 | if err != nil {
27 | return err
28 | }
29 | defer listener.Close()
30 |
31 | // Устанавлием права на сокет
32 | if err := os.Chmod(fileName, 0666); err != nil {
33 | return err
34 | }
35 |
36 | // Обработка входящих соединений
37 | for {
38 | conn, err := listener.Accept()
39 | if err != nil {
40 | if errors.Is(err, net.ErrClosed) {
41 | return nil
42 | }
43 | continue
44 | }
45 |
46 | go handleConnection(conn, handler)
47 | }
48 | }
49 |
50 | // handleConnection обрабатывает одно соединение
51 | func handleConnection(conn net.Conn, handler func(string) ([]byte, error)) {
52 | defer conn.Close()
53 |
54 | buf := make([]byte, 4096)
55 | n, err := conn.Read(buf)
56 | if err != nil {
57 | return
58 | }
59 |
60 | command := string(buf[:n])
61 | response, err := handler(command)
62 | if err != nil {
63 | // Форматируем ошибку в стандартный формат
64 | response = []byte("error: " + err.Error() + "\n")
65 | }
66 |
67 | conn.Write(response)
68 | }
69 |
70 | // Send отправляет команду в IPC сокет
71 | // command: строка команды (например "j/layer get")
72 | func Send(fileName string, command string) ([]byte, error) {
73 | conn, err := net.Dial("unix", fileName)
74 | if err != nil {
75 | return nil, err
76 | }
77 | defer conn.Close()
78 |
79 | _, err = conn.Write([]byte(command + "\n"))
80 | if err != nil {
81 | return nil, err
82 | }
83 |
84 | buf := make([]byte, 4096)
85 | n, err := conn.Read(buf)
86 | if err != nil {
87 | return nil, err
88 | }
89 |
90 | return buf[:n], nil
91 | }
92 |
93 | // StopServer останавливает сервер (вспомогательная функция)
94 | func StopServer(fileName string) error {
95 | return syscall.Unlink(fileName)
96 | }
97 | ```
98 |
99 | ## **Использование модуля**
100 |
101 | ### **1. Серверная часть**
102 | ```go
103 | package main
104 |
105 | import (
106 | "fmt"
107 | "dockIPC"
108 | )
109 |
110 | func main() {
111 | // Обработчик команд
112 | handler := func(command string) ([]byte, error) {
113 | switch command {
114 | case "j/layer get":
115 | return []byte(`{"layers":["bottom","top"]}`), nil
116 | case "layer get":
117 | return []byte("bottom\ntop"), nil
118 | default:
119 | return nil, fmt.Errorf("unknown command")
120 | }
121 | }
122 |
123 | // Запуск сервера
124 | err := dockIPC.StartServer("/tmp/hypr-dock.sock", handler)
125 | if err != nil {
126 | panic(err)
127 | }
128 | }
129 | ```
130 |
131 | ### **2. Клиентская часть**
132 | ```go
133 | package main
134 |
135 | import (
136 | "fmt"
137 | "dockIPC"
138 | )
139 |
140 | func main() {
141 | // Отправка команды
142 | response, err := dockIPC.Send("/tmp/hypr-dock.sock", "j/layer get")
143 | if err != nil {
144 | fmt.Println("Error:", err)
145 | return
146 | }
147 |
148 | fmt.Println("Response:", string(response))
149 | }
150 | ```
151 |
152 | ## **Особенности реализации**
153 |
154 | 1. **Безопасность**:
155 | - Удаление старого сокета перед созданием
156 | - Установка прав 0666 на сокет
157 | - Корректная обработка закрытия соединений
158 |
159 | 2. **Производительность**:
160 | - Каждое соединение обрабатывается в отдельной goroutine
161 | - Буферизированное чтение (4096 байт)
162 |
163 | 3. **Гибкость**:
164 | - Обработчик команд может возвращать любые бинарные данные
165 | - Поддержка текстовых и JSON-команд
166 |
167 | 4. **Вспомогательные функции**:
168 | - `StopServer()` для корректного завершения
169 | - Автоматическое форматирование ошибок
170 |
171 | Модуль готов к интеграции в проект `hypr-dock-ctl` и может быть расширен при необходимости.
--------------------------------------------------------------------------------
/internal/state/state.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "hypr-dock/internal/itemsctl"
5 | "hypr-dock/internal/pkg/timer"
6 | "hypr-dock/internal/pvctl"
7 | "hypr-dock/internal/settings"
8 | "sync"
9 |
10 | "github.com/dlasky/gotk3-layershell/layershell"
11 | "github.com/gotk3/gotk3/glib"
12 | "github.com/gotk3/gotk3/gtk"
13 | )
14 |
15 | type State struct {
16 | Settings settings.Settings
17 | Window *gtk.Window
18 | SignalHandlers map[string]glib.SignalHandle
19 | DetectArea *gtk.Window
20 | ItemsBox *gtk.Box
21 | Orientation gtk.Orientation
22 | Edge layershell.LayerShellEdgeFlags
23 | DockHideTimer *timer.Timer
24 | List *itemsctl.List
25 | Special bool
26 | PV *pvctl.PV
27 | ContextOpen bool
28 | mu sync.Mutex
29 | }
30 |
31 | func New(settings settings.Settings) *State {
32 | return &State{
33 | Settings: settings,
34 | DockHideTimer: timer.New(),
35 | List: itemsctl.New(),
36 | PV: pvctl.New(settings),
37 | }
38 | }
39 |
40 | func (s *State) GetList() *itemsctl.List {
41 | s.mu.Lock()
42 | defer s.mu.Unlock()
43 |
44 | return s.List
45 | }
46 |
47 | func (s *State) GetDockHideTimer() *timer.Timer {
48 | s.mu.Lock()
49 | defer s.mu.Unlock()
50 |
51 | return s.DockHideTimer
52 | }
53 |
54 | func (s *State) AddSignalHandler(name string, id glib.SignalHandle) {
55 | s.mu.Lock()
56 | defer s.mu.Unlock()
57 |
58 | if s.SignalHandlers == nil {
59 | s.SignalHandlers = make(map[string]glib.SignalHandle)
60 | }
61 | s.SignalHandlers[name] = id
62 | }
63 |
64 | func (s *State) RemoveSignalHandler(name string, window *gtk.Window) {
65 | s.mu.Lock()
66 | defer s.mu.Unlock()
67 |
68 | if id, exists := s.SignalHandlers[name]; exists {
69 | window.HandlerDisconnect(id)
70 | delete(s.SignalHandlers, name)
71 | }
72 | }
73 |
74 | func (s *State) SetEdge(edge layershell.LayerShellEdgeFlags) {
75 | s.mu.Lock()
76 | defer s.mu.Unlock()
77 | s.Edge = edge
78 | }
79 |
80 | func (s *State) GetEdge() layershell.LayerShellEdgeFlags {
81 | s.mu.Lock()
82 | defer s.mu.Unlock()
83 | return s.Edge
84 | }
85 |
86 | func (s *State) SetSettings(settings settings.Settings) {
87 | s.mu.Lock()
88 | defer s.mu.Unlock()
89 | s.Settings = settings
90 | }
91 |
92 | func (s *State) GetSettings() settings.Settings {
93 | s.mu.Lock()
94 | defer s.mu.Unlock()
95 | return s.Settings
96 | }
97 |
98 | func (s *State) GetPinned() *[]string {
99 | s.mu.Lock()
100 | defer s.mu.Unlock()
101 | return &s.Settings.PinnedApps
102 | }
103 |
104 | func (s *State) Update(fn func(*State)) {
105 | s.mu.Lock()
106 | defer s.mu.Unlock()
107 | fn(s)
108 | }
109 |
110 | func (s *State) SetWindow(window *gtk.Window) {
111 | s.mu.Lock()
112 | defer s.mu.Unlock()
113 | s.Window = window
114 | }
115 |
116 | func (s *State) SetDetectArea(window *gtk.Window) {
117 | s.mu.Lock()
118 | defer s.mu.Unlock()
119 | s.DetectArea = window
120 | }
121 |
122 | func (s *State) SetItemsBox(box *gtk.Box) {
123 | s.mu.Lock()
124 | defer s.mu.Unlock()
125 | s.ItemsBox = box
126 | }
127 |
128 | func (s *State) SetOrientation(orientation gtk.Orientation) {
129 | s.mu.Lock()
130 | defer s.mu.Unlock()
131 | s.Orientation = orientation
132 | }
133 |
134 | func (s *State) SetSpecial(is bool) {
135 | s.mu.Lock()
136 | defer s.mu.Unlock()
137 | s.Special = is
138 | }
139 |
140 | func (s *State) GetWindow() *gtk.Window {
141 | s.mu.Lock()
142 | defer s.mu.Unlock()
143 | return s.Window
144 | }
145 |
146 | func (s *State) GetDetectArea() *gtk.Window {
147 | s.mu.Lock()
148 | defer s.mu.Unlock()
149 | return s.DetectArea
150 | }
151 |
152 | func (s *State) GetItemsBox() *gtk.Box {
153 | s.mu.Lock()
154 | defer s.mu.Unlock()
155 | return s.ItemsBox
156 | }
157 |
158 | func (s *State) GetOrientation() gtk.Orientation {
159 | s.mu.Lock()
160 | defer s.mu.Unlock()
161 | return s.Orientation
162 | }
163 |
164 | func (s *State) GetSpecial() bool {
165 | s.mu.Lock()
166 | defer s.mu.Unlock()
167 | return s.Special
168 | }
169 |
170 | func (s *State) GetPV() *pvctl.PV {
171 | s.mu.Lock()
172 | defer s.mu.Unlock()
173 |
174 | return s.PV
175 | }
176 |
177 | func (s *State) GetContextOpen() bool {
178 | s.mu.Lock()
179 | defer s.mu.Unlock()
180 |
181 | return s.ContextOpen
182 | }
183 |
184 | func (s *State) SetContextOpen(flag bool) {
185 | s.mu.Lock()
186 | defer s.mu.Unlock()
187 |
188 | s.ContextOpen = flag
189 | }
190 |
--------------------------------------------------------------------------------
/internal/layering/layering.go:
--------------------------------------------------------------------------------
1 | package layering
2 |
3 | import (
4 | detectzone "hypr-dock/internal/detectZone"
5 | "hypr-dock/internal/state"
6 | "hypr-dock/pkg/ipc"
7 | "strings"
8 |
9 | "github.com/dlasky/gotk3-layershell/layershell"
10 | "github.com/gotk3/gotk3/gdk"
11 | "github.com/gotk3/gotk3/gtk"
12 | )
13 |
14 | func SetWindowProperty(appState *state.State) {
15 | window := appState.GetWindow()
16 | settings := appState.GetSettings()
17 |
18 | layershell.InitForWindow(window)
19 | layershell.SetNamespace(window, "hypr-dock")
20 |
21 | ChangePosition(settings.Position, appState)
22 | ChangeLayer(settings.Layer, appState)
23 | }
24 |
25 | func ChangeLayer(layer string, appState *state.State) {
26 | window := appState.GetWindow()
27 | if window == nil {
28 | return
29 | }
30 |
31 | layers := map[string]layershell.LayerShellLayerFlags{
32 | "background": layershell.LAYER_SHELL_LAYER_BACKGROUND,
33 | "bottom": layershell.LAYER_SHELL_LAYER_BOTTOM,
34 | "top": layershell.LAYER_SHELL_LAYER_TOP,
35 | "overlay": layershell.LAYER_SHELL_LAYER_OVERLAY,
36 | }
37 |
38 | if layer == "auto" {
39 | layershell.SetLayer(window, layers["bottom"])
40 | layershell.SetExclusiveZone(window, 0)
41 | AutoLayer(appState)
42 | detectzone.Init(appState)
43 | return
44 | }
45 |
46 | DisableAutoLayer(appState)
47 | if strings.Contains(layer, "exclusive") {
48 | exLayer := strings.Split(layer, "-")[1]
49 | layershell.SetLayer(window, layers[exLayer])
50 | layershell.AutoExclusiveZoneEnable(window)
51 | return
52 | }
53 |
54 | layershell.SetLayer(window, layers[layer])
55 | }
56 |
57 | func ChangePosition(position string, appState *state.State) {
58 | window := appState.GetWindow()
59 | if window == nil {
60 | return
61 | }
62 |
63 | oreintations := map[string]gtk.Orientation{
64 | "bottom": gtk.ORIENTATION_HORIZONTAL,
65 | "top": gtk.ORIENTATION_HORIZONTAL,
66 | "left": gtk.ORIENTATION_VERTICAL,
67 | "right": gtk.ORIENTATION_VERTICAL,
68 | }
69 |
70 | edges := map[string]layershell.LayerShellEdgeFlags{
71 | "bottom": layershell.LAYER_SHELL_EDGE_BOTTOM,
72 | "top": layershell.LAYER_SHELL_EDGE_TOP,
73 | "left": layershell.LAYER_SHELL_EDGE_LEFT,
74 | "right": layershell.LAYER_SHELL_EDGE_RIGHT,
75 | }
76 |
77 | layershell.SetAnchor(window, edges[position], true)
78 | layershell.SetMargin(window, edges[position], 0)
79 |
80 | appState.SetOrientation(oreintations[position])
81 | appState.SetEdge(edges[position])
82 | }
83 |
84 | func AutoLayer(appState *state.State) {
85 | DisableAutoLayer(appState)
86 | window := appState.GetWindow()
87 |
88 | ipc.AddEventListener("hd>>pv-pointer-enter", func(e string) {
89 | appState.GetDockHideTimer().Stop()
90 | }, true)
91 |
92 | ipc.AddEventListener("hd>>pv-pointer-leave", func(e string) {
93 | DispathLeaveEvent(window, nil, appState)
94 | }, true)
95 |
96 | ipc.AddEventListener("hd>>focus-window", func(e string) {
97 | DispathLeaveEvent(window, nil, appState)
98 | }, true)
99 |
100 | enterSig := window.Connect("enter-notify-event", func(window *gtk.Window, e *gdk.Event) {
101 | event := gdk.EventCrossingNewFromEvent(e)
102 | isInWindow := event.Detail() == 3 || event.Detail() == 4
103 |
104 | if !isInWindow || appState.GetSpecial() {
105 | return
106 | }
107 |
108 | timer := appState.GetDockHideTimer()
109 |
110 | timer.Stop()
111 | layershell.SetLayer(window, layershell.LAYER_SHELL_LAYER_TOP)
112 | })
113 | appState.AddSignalHandler("enter", enterSig)
114 |
115 | leaveSig := window.Connect("leave-notify-event", func(window *gtk.Window, e *gdk.Event) {
116 | DispathLeaveEvent(window, e, appState)
117 | })
118 | appState.AddSignalHandler("leave", leaveSig)
119 | }
120 |
121 | func DispathLeaveEvent(window *gtk.Window, e *gdk.Event, appState *state.State) {
122 | isInWindow := true
123 | if e != nil {
124 | event := gdk.EventCrossingNewFromEvent(e)
125 | isInWindow = event.Detail() == 3 || event.Detail() == 4
126 | }
127 |
128 | if !isInWindow {
129 | return
130 | }
131 |
132 | timer := appState.GetDockHideTimer()
133 |
134 | timer.Run(appState.Settings.AutoHideDeley, func() {
135 | layershell.SetLayer(window, layershell.LAYER_SHELL_LAYER_BOTTOM)
136 | })
137 | }
138 |
139 | func DisableAutoLayer(appState *state.State) {
140 | detectArea := appState.GetDetectArea()
141 | if detectArea != nil {
142 | detectArea.Destroy()
143 | appState.SetDetectArea(nil)
144 | }
145 |
146 | window := appState.GetWindow()
147 |
148 | appState.RemoveSignalHandler("enter", window)
149 | appState.RemoveSignalHandler("leave", window)
150 | }
151 |
--------------------------------------------------------------------------------
/internal/item/popup.go:
--------------------------------------------------------------------------------
1 | package item
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/gotk3/gotk3/gtk"
9 | "github.com/gotk3/gotk3/pango"
10 |
11 | "hypr-dock/internal/pkg/desktop"
12 | "hypr-dock/internal/pkg/utils"
13 | "hypr-dock/internal/placeholders"
14 | "hypr-dock/internal/settings"
15 | "hypr-dock/pkg/ipc"
16 | )
17 |
18 | func (item *Item) WindowsMenu() (*gtk.Menu, error) {
19 | menu, err := gtk.MenuNew()
20 | if err != nil {
21 | log.Println(err)
22 | }
23 |
24 | desktopData := desktop.New(item.ClassName)
25 |
26 | AddWindowsItemToMenu(menu, item.Windows, desktopData)
27 |
28 | menu.SetName("windows-menu")
29 | menu.ShowAll()
30 |
31 | return menu, nil
32 | }
33 |
34 | func (item *Item) ContextMenu(settings settings.Settings) (*gtk.Menu, error) {
35 | menu, err := gtk.MenuNew()
36 | if err != nil {
37 | log.Println(err)
38 | }
39 |
40 | desktopData := item.DesktopData
41 |
42 | AddWindowsItemToMenu(menu, item.Windows, desktopData)
43 |
44 | if item.Instances != 0 {
45 | separator, err := gtk.SeparatorMenuItemNew()
46 | if err == nil {
47 | menu.Append(separator)
48 | } else {
49 | log.Println(err)
50 | }
51 | }
52 |
53 | if item.Actions != nil {
54 | for _, action := range item.Actions {
55 | exec := func() {
56 | utils.Launch(action.Exec)
57 | }
58 |
59 | var actionMenuItem *gtk.MenuItem
60 | var err error
61 |
62 | if action.Icon == "" {
63 | actionMenuItem, err = BuildContextItem(action.Name, exec)
64 | } else {
65 | actionMenuItem, err = BuildContextItem(action.Name, exec, action.Icon)
66 | }
67 |
68 | if err == nil {
69 | menu.Append(actionMenuItem)
70 | } else {
71 | log.Println(err)
72 | }
73 | }
74 |
75 | separator, err := gtk.SeparatorMenuItemNew()
76 | if err == nil {
77 | menu.Append(separator)
78 | } else {
79 | log.Println(err)
80 | }
81 | }
82 |
83 | launchMenuItem, err := BuildLaunchMenuItem(item, desktopData.Exec)
84 | if err == nil {
85 | menu.Append(launchMenuItem)
86 | } else {
87 | log.Println(err)
88 | }
89 |
90 | pinMenuItem, err := BuildPinMenuItem(item, settings)
91 | if err == nil {
92 | menu.Append(pinMenuItem)
93 | } else {
94 | log.Println(err)
95 | }
96 |
97 | if item.Instances == 1 {
98 | closeMenuItem, err := BuildContextItem("Close", func() {
99 | ipc.Hyprctl("dispatch closewindow address:" + item.Windows[0]["Address"])
100 | }, "close-symbolic")
101 | if err == nil {
102 | menu.Append(closeMenuItem)
103 | } else {
104 | log.Println(err)
105 | }
106 | }
107 |
108 | menu.SetName("context-menu")
109 | menu.ShowAll()
110 |
111 | return menu, nil
112 | }
113 |
114 | func AddWindowsItemToMenu(menu *gtk.Menu, windows []map[string]string, desktopData *desktop.Desktop) {
115 | for _, window := range windows {
116 | menuItem, err := BuildContextItem(window["Title"], func() {
117 | go ipc.Hyprctl("dispatch focuswindow address:" + window["Address"])
118 | }, desktopData.Icon)
119 |
120 | if err != nil {
121 | log.Println(err)
122 | continue
123 | }
124 |
125 | menu.Append(menuItem)
126 | }
127 | }
128 |
129 | func BuildLaunchMenuItem(item *Item, exec string) (*gtk.MenuItem, error) {
130 | if item.Instances != 0 && item.DesktopData.SingleWindow {
131 | return nil, errors.New("")
132 | }
133 |
134 | labelText := item.DesktopData.Name
135 | if item.Instances != 0 {
136 | labelText = "New Window - " + labelText
137 | }
138 |
139 | launchMenuItem, err := BuildContextItem(labelText, func() {
140 | placeholders.Run(exec)
141 | }, item.DesktopData.Icon)
142 |
143 | if err != nil {
144 | return nil, err
145 | }
146 |
147 | return launchMenuItem, nil
148 | }
149 |
150 | func BuildPinMenuItem(item *Item, settings settings.Settings) (*gtk.MenuItem, error) {
151 | labelText := "Pin"
152 | if item.IsPinned() {
153 | labelText = "Unpin"
154 | }
155 |
156 | menuItem, err := BuildContextItem(labelText, func() {
157 | item.TogglePin(settings)
158 | })
159 |
160 | if err != nil {
161 | return nil, err
162 | }
163 |
164 | return menuItem, nil
165 | }
166 |
167 | func BuildContextItem(labelText string, connectFunc func(), iconName ...string) (*gtk.MenuItem, error) {
168 | size := 16
169 | spacing := 6
170 |
171 | menuItem, err := gtk.MenuItemNew()
172 | if err != nil {
173 | return nil, err
174 | }
175 |
176 | menuItem.SetName("menu-item")
177 |
178 | hbox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, spacing)
179 | if err != nil {
180 | return nil, err
181 | }
182 |
183 | hbox.SetName("hbox")
184 | /* Hack (HELP ME)*/
185 | /* stackoverflow.com/questions/48452717/how-to-replace-the-deprecated-gtk3-gtkimagemenuitem */
186 | utils.AddStyle(hbox, fmt.Sprintf("#hbox {margin-left: %dpx;}", 0-(size+spacing)))
187 |
188 | label, err := gtk.LabelNew(labelText)
189 | if err != nil {
190 | return nil, err
191 | }
192 |
193 | label.SetEllipsize(pango.ELLIPSIZE_END)
194 | label.SetMaxWidthChars(30)
195 |
196 | if len(iconName) > 0 {
197 | icon, err := utils.CreateImage(iconName[0], size)
198 | if err == nil {
199 | hbox.Add(icon)
200 | }
201 | } else {
202 | label.SetMarginStart(size + spacing)
203 | }
204 |
205 | if connectFunc != nil {
206 | menuItem.Connect("activate", func() {
207 | connectFunc()
208 | })
209 | }
210 |
211 | hbox.Add(label)
212 | menuItem.SetReserveIndicator(false)
213 | menuItem.Add(hbox)
214 |
215 | return menuItem, nil
216 | }
217 |
--------------------------------------------------------------------------------
/internal/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "slices"
8 |
9 | "github.com/gotk3/gotk3/gtk"
10 |
11 | "hypr-dock/internal/btnctl"
12 | "hypr-dock/internal/hypr/hyprOpt"
13 | "hypr-dock/internal/item"
14 | "hypr-dock/internal/pkg/utils"
15 | "hypr-dock/internal/state"
16 | "hypr-dock/pkg/ipc"
17 | )
18 |
19 | func BuildApp(appState *state.State) *gtk.Box {
20 | settings := appState.GetSettings()
21 | orientation := appState.GetOrientation()
22 |
23 | app, err := gtk.BoxNew(orientation, 0)
24 | if err != nil {
25 | log.Println("BuildApp() | app | gtk.BoxNew()")
26 | log.Fatal(err)
27 | }
28 |
29 | addWindowMarginRule(app, appState)
30 | app.SetName("app")
31 |
32 | itemsBox, _ := gtk.BoxNew(orientation, settings.Spacing)
33 | itemsBox.SetName("items-box")
34 |
35 | switch orientation {
36 | case gtk.ORIENTATION_HORIZONTAL:
37 | itemsBox.SetMarginEnd(int(float64(settings.Spacing) * 0.8))
38 | itemsBox.SetMarginStart(int(float64(settings.Spacing) * 0.8))
39 | case gtk.ORIENTATION_VERTICAL:
40 | itemsBox.SetMarginBottom(int(float64(settings.Spacing) * 0.8))
41 | itemsBox.SetMarginTop(int(float64(settings.Spacing) * 0.8))
42 | }
43 |
44 | appState.SetItemsBox(itemsBox)
45 | renderItems(appState)
46 | app.Add(itemsBox)
47 |
48 | return app
49 | }
50 |
51 | func renderItems(appState *state.State) {
52 | clients, _ := ipc.GetClients()
53 |
54 | for _, className := range *appState.GetPinned() {
55 | InitNewItemInClass(className, appState)
56 | }
57 |
58 | for _, ipcClient := range clients {
59 | InitNewItemInIPC(ipcClient, appState)
60 | }
61 |
62 | ipc.DispatchEvent("hd>>dock-render-finish")
63 | }
64 |
65 | func InitNewItemInIPC(ipcClient ipc.Client, appState *state.State) {
66 | list := appState.GetList()
67 | className := ipcClient.Class
68 | if !slices.Contains(*appState.GetPinned(), className) && list.Get(className) == nil {
69 | InitNewItemInClass(className, appState)
70 | }
71 |
72 | list.Get(className).UpdateState(ipcClient, appState.GetSettings())
73 | appState.GetWindow().ShowAll()
74 | }
75 |
76 | func InitNewItemInClass(className string, appState *state.State) {
77 | list := appState.GetList()
78 | item, err := item.New(className, appState.GetSettings())
79 | if err != nil {
80 | log.Println(err)
81 | return
82 | }
83 |
84 | btnctl.Dispatch(item, appState)
85 |
86 | item.List = list.GetMap()
87 | item.PinnedList = appState.GetPinned()
88 | list.Add(className, item)
89 |
90 | appState.GetItemsBox().Add(item.ButtonBox)
91 | appState.GetWindow().ShowAll()
92 | }
93 |
94 | func RemoveApp(address string, appState *state.State) {
95 | item, windowIndex, err := searchByAddress(address, appState)
96 | if err != nil {
97 | log.Println(err)
98 | return
99 | }
100 |
101 | className := item.ClassName
102 | if item.Instances == 1 && !slices.Contains(*appState.GetPinned(), className) {
103 | item.Remove()
104 | return
105 | }
106 |
107 | item.RemoveLastInstance(windowIndex, appState.GetSettings())
108 |
109 | appState.GetWindow().ShowAll()
110 | }
111 |
112 | func searchByAddress(address string, appState *state.State) (*item.Item, int, error) {
113 | for _, item := range appState.GetList().GetMap() {
114 | for windowIndex, window := range item.Windows {
115 | if window["Address"] == address {
116 | return item, windowIndex, nil
117 | }
118 | }
119 | }
120 |
121 | err := errors.New("Window not found: " + address)
122 | return nil, 0, err
123 | }
124 |
125 | func ChangeWindowTitle(address string, title string, appState *state.State) {
126 | for _, data := range appState.GetList().GetMap() {
127 | for _, appWindow := range data.Windows {
128 | if appWindow["Address"] == address {
129 | appWindow["Title"] = title
130 | }
131 | }
132 | }
133 | }
134 |
135 | func addWindowMarginRule(app *gtk.Box, appState *state.State) {
136 | settings := appState.GetSettings()
137 | position := settings.Position
138 | var marginProvider *gtk.CssProvider
139 |
140 | switch settings.SystemGapUsed {
141 | case "true":
142 | margin, err := hyprOpt.GetGap()
143 | if err != nil {
144 | log.Println(err, "\nSet margin in config")
145 | applyWindowMarginCSS(app, position, settings.Margin)
146 | }
147 |
148 | marginProvider = applyWindowMarginCSS(app, position, margin[0])
149 |
150 | hyprOpt.GapChangeEvent(func(gap int) {
151 | utils.RemoveStyleProvider(app, marginProvider)
152 | marginProvider = applyWindowMarginCSS(app, position, gap)
153 | log.Println("Window margins updated successfully: ", gap)
154 | })
155 | case "false":
156 | applyWindowMarginCSS(app, position, settings.Margin)
157 | }
158 | }
159 |
160 | func applyWindowMarginCSS(app *gtk.Box, position string, margin int) *gtk.CssProvider {
161 | css := fmt.Sprintf("#app {margin-%s: %dpx;}", position, margin)
162 |
163 | marginProvider, err := gtk.CssProviderNew()
164 | if err != nil {
165 | log.Printf("Failed to create CSS provider: %v", err)
166 | return nil
167 | }
168 |
169 | appStyleContext, err := app.GetStyleContext()
170 | if err != nil {
171 | log.Printf("Failed to get style context: %v", err)
172 | return nil
173 | }
174 |
175 | appStyleContext.AddProvider(marginProvider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
176 |
177 | err = marginProvider.LoadFromData(css)
178 | if err != nil {
179 | log.Printf("Failed to load CSS data: %v", err)
180 | return nil
181 | }
182 |
183 | return marginProvider
184 | }
185 |
--------------------------------------------------------------------------------
/docs/tasks/ipc/dockIPC-task.md:
--------------------------------------------------------------------------------
1 | # **Проектная документация модуля dockIPC**
2 |
3 | ## **1. Концепция модуля**
4 |
5 | ### **1.1 Назначение**
6 | Модуль `dockIPC` предоставляет минималистичный API для организации межпроцессного взаимодействия (IPC) через Unix domain sockets. Основные цели:
7 | - Создание стабильного канала коммуникации между демоном и клиентами
8 | - Поддержка синхронного запрос-ответ взаимодействия
9 | - Обеспечение простой интеграции без сложных зависимостей
10 |
11 | ### **1.2 Ключевые принципы**
12 | 1. **Минимализм** - только базовые функции для IPC
13 | 2. **Идемпотентность** - повторные вызовы дают одинаковый результат
14 | 3. **Атомарность** - каждая операция либо завершается полностью, либо не выполняется
15 | 4. **Потокобезопасность** - корректная работа в многопоточной среде
16 |
17 | ---
18 |
19 | ## **2. Архитектурные компоненты**
20 |
21 | ### **2.1 Серверная часть**
22 | #### **Алгоритм работы:**
23 | 1. **Инициализация сокета**
24 | - Удаление существующего файла сокета (атомарная операция)
25 | - Создание нового Unix domain socket
26 | - Установка прав доступа (0666)
27 |
28 | 2. **Цикл обработки соединений**
29 | - Принятие входящего соединения (блокирующая операция)
30 | - Запуск обработчика соединения в отдельной goroutine
31 | - Чтение команды из сокета (максимальный размер - 4КБ)
32 | - Вызов пользовательского обработчика
33 | - Отправка результата обратно клиенту
34 |
35 | 3. **Обработка ошибок**
36 | - Автоматическое закрытие соединений при ошибках
37 | - Преобразование ошибок обработчика в стандартный формат
38 |
39 | ### **2.2 Клиентская часть**
40 | #### **Алгоритм работы:**
41 | 1. **Установка соединения**
42 | - Попытка подключения к существующему сокету
43 | - Таймаут по умолчанию (системный)
44 |
45 | 2. **Передача команды**
46 | - Запись строки команды в сокет
47 | - Добавление терминатора `\n` для четкого разделения команд
48 |
49 | 3. **Получение ответа**
50 | - Чтение данных из сокета (блокирующая операция)
51 | - Возврат сырых бинарных данных
52 |
53 | ---
54 |
55 | ## **3. Взаимодействие компонентов**
56 |
57 | ```mermaid
58 | sequenceDiagram
59 | participant Client
60 | participant Server
61 | participant Handler
62 |
63 | Client->>Server: Установка соединения (Dial)
64 | Client->>Server: Отправка команды + \n
65 | Server->>Handler: Вызов обработчика с командой
66 | Handler->>Server: Возврат данных/ошибки
67 | Server->>Client: Отправка сырого ответа
68 | Client->>Server: Закрытие соединения
69 | ```
70 |
71 | ### **3.1 Особенности взаимодействия**
72 | 1. **Синхронная модель** - клиент блокируется до получения ответа
73 | 2. **Статический буфер** - фиксированный размер 4КБ для простоты
74 | 3. **Гарантия доставки** - либо полный ответ, либо ошибка
75 |
76 | ---
77 |
78 | ## **4. Алгоритмы обработки**
79 |
80 | ### **4.1 Обработка соединений (Server)**
81 | 1. **Мультиплексирование**:
82 | - Каждое соединение обрабатывается в отдельной goroutine
83 | - Нет ограничения на количество одновременных соединений
84 |
85 | 2. **Управление ресурсами**:
86 | - Деферы для гарантированного закрытия соединений
87 | - Автоматическое восстановление после ошибок
88 |
89 | 3. **Формат обмена**:
90 | - Команды - строки в UTF-8
91 | - Ответы - произвольные бинарные данные
92 |
93 | ### **4.2 Обработка ошибок**
94 | 1. **Типы ошибок**:
95 | - Ошибки соединения (файл сокета не существует)
96 | - Ошибки чтения/записи
97 | - Ошибки пользовательского обработчика
98 |
99 | 2. **Стратегии**:
100 | - Автоматическое повторное создание сокета
101 | - Преобразование ошибок в стандартный формат
102 | - Гарантированное освобождение ресурсов
103 |
104 | ---
105 |
106 | ## **5. Концепция реализации**
107 |
108 | ### **5.1 Сервер**
109 | **Ключевые идеи:**
110 | 1. **Один сокет - много соединений**:
111 | - Основной цикл принимает соединения
112 | - Каждое соединение - отдельный экземпляр обработчика
113 |
114 | 2. **Изоляция состояний**:
115 | - Нет разделяемого состояния между обработчиками
116 | - Все зависимости инжектятся через замыкания
117 |
118 | 3. **Гибкость обработки**:
119 | - Пользовательский обработчик может реализовывать любую логику
120 | - Поддержка как текстовых, так и бинарных протоколов
121 |
122 | ### **5.2 Клиент**
123 | **Ключевые идеи:**
124 | 1. **Простота использования**:
125 | - Одна функция для отправки команд
126 | - Прозрачное управление соединением
127 |
128 | 2. **Детерминированность**:
129 | - Четкое разделение команд через `\n`
130 | - Предсказуемый размер буфера
131 |
132 | 3. **Расширяемость**:
133 | - Возможность добавления таймаутов
134 | - Поддержка разных стратегий сериализации
135 |
136 | ---
137 |
138 | ## **6. Ограничения и допущения**
139 |
140 | 1. **Размер сообщений**:
141 | - Максимальный размер - 4КБ (можно увеличить при необходимости)
142 | - Нет поддержки потоковой передачи
143 |
144 | 2. **Безопасность**:
145 | - Нет шифрования передаваемых данных
146 | - Аутентификация через права файловой системы
147 |
148 | 3. **Производительность**:
149 | - Оптимизировано для low-throughput коммуникации
150 | - Не подходит для high-frequency RPC
151 |
152 | ---
153 |
154 | ## **7. Перспективы развития**
155 |
156 | 1. **Добавление**:
157 | - Таймаутов соединения
158 | - Поддержки потоковой передачи
159 | - Механизма аутентификации
160 |
161 | 2. **Оптимизация**:
162 | - Пула соединений
163 | - Буферизированного ввода/вывода
164 | - Более эффективного аллокатора памяти
165 |
166 | Модуль специально спроектирован для постепенного расширения функциональности при сохранении базовых принципов простоты и надежности.
--------------------------------------------------------------------------------
/internal/item/item.go:
--------------------------------------------------------------------------------
1 | package item
2 |
3 | import (
4 | "log"
5 | "slices"
6 |
7 | "github.com/gotk3/gotk3/gdk"
8 | "github.com/gotk3/gotk3/gtk"
9 |
10 | "hypr-dock/internal/pkg/cfg"
11 | "hypr-dock/internal/pkg/desktop"
12 | "hypr-dock/internal/pkg/indicator"
13 | "hypr-dock/internal/pkg/utils"
14 | "hypr-dock/internal/settings"
15 |
16 | "hypr-dock/pkg/ipc"
17 | )
18 |
19 | type Item struct {
20 | Instances int
21 | Windows []map[string]string
22 | DesktopData *desktop.Desktop
23 | DesktopDataV2 *desktop.Desktop2
24 | Actions []*desktop.Action
25 | ClassName string
26 | Button *gtk.Button
27 | ButtonBox *gtk.Box
28 | IndicatorImage *gtk.Image
29 | List map[string]*Item
30 | PinnedList *[]string
31 | }
32 |
33 | func New(className string, settings settings.Settings) (*Item, error) {
34 | desktopData := desktop.New(className)
35 | desktopDataV2, err := desktop.NewV2(className)
36 | if err != nil {
37 | log.Println(err)
38 | }
39 |
40 | orientation := gtk.ORIENTATION_VERTICAL
41 | switch settings.Position {
42 | case "left", "right":
43 | orientation = gtk.ORIENTATION_HORIZONTAL
44 | }
45 |
46 | item, err := gtk.BoxNew(orientation, 0)
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | indicatorImage, err := indicator.New(0, settings)
52 | if err == nil {
53 | appendInducator(item, indicatorImage, settings.Position)
54 | } else {
55 | log.Println(err)
56 | }
57 |
58 | button, err := gtk.ButtonNew()
59 | if err == nil {
60 | image, err := utils.CreateImage(desktopData.Icon, settings.IconSize)
61 | if err == nil {
62 | button.SetImage(image)
63 | } else {
64 | log.Println(err)
65 | }
66 |
67 | button.SetName(className)
68 |
69 | button.SetTooltipText(desktopData.Name)
70 |
71 | display, err := gdk.DisplayGetDefault()
72 | if err == nil {
73 | pointer, _ := gdk.CursorNewFromName(display, "pointer")
74 | arrow, _ := gdk.CursorNewFromName(display, "default")
75 |
76 | button.Connect("enter-notify-event", func() {
77 | win, _ := button.GetWindow()
78 | if win != nil {
79 | win.SetCursor(pointer)
80 | }
81 | })
82 |
83 | button.Connect("leave-notify-event", func() {
84 | win, _ := button.GetWindow()
85 | if win != nil {
86 | win.SetCursor(arrow)
87 | }
88 | })
89 | }
90 |
91 | item.Add(button)
92 | } else {
93 | log.Println(err)
94 | }
95 |
96 | actions, err := desktop.GetAppActions(className)
97 | if err != nil {
98 | log.Println(err)
99 | }
100 |
101 | return &Item{
102 | IndicatorImage: indicatorImage,
103 | Button: button,
104 | ButtonBox: item,
105 | DesktopData: desktopData,
106 | DesktopDataV2: desktopDataV2,
107 | Actions: actions,
108 | Instances: 0,
109 | ClassName: className,
110 | List: nil,
111 | PinnedList: nil,
112 | }, nil
113 | }
114 |
115 | func (item *Item) RemoveLastInstance(windowIndex int, settings settings.Settings) {
116 | if item.IndicatorImage != nil {
117 | item.IndicatorImage.Destroy()
118 | }
119 |
120 | newImage, err := indicator.New(item.Instances-1, settings)
121 | if err == nil {
122 | appendInducator(item.ButtonBox, newImage, settings.Position)
123 | }
124 |
125 | item.Instances -= 1
126 | item.Windows = utils.RemoveFromSlice(item.Windows, windowIndex)
127 | item.IndicatorImage = newImage
128 |
129 | if item.Instances == 0 {
130 | item.Button.SetTooltipText(item.DesktopData.Name)
131 | }
132 | }
133 |
134 | func (item *Item) UpdateState(ipcClient ipc.Client, settings settings.Settings) {
135 | appWindow := map[string]string{
136 | "Address": ipcClient.Address,
137 | "Title": ipcClient.Title,
138 | }
139 |
140 | if item.IndicatorImage != nil {
141 | item.IndicatorImage.Destroy()
142 | }
143 |
144 | indicatorImage, err := indicator.New(item.Instances+1, settings)
145 | if err == nil {
146 | // item.ButtonBox.Add(indicatorImage)
147 | appendInducator(item.ButtonBox, indicatorImage, settings.Position)
148 | }
149 |
150 | item.Windows = append(item.Windows, appWindow)
151 | item.IndicatorImage = indicatorImage
152 | item.Instances += 1
153 |
154 | if item.Instances != 0 {
155 | item.Button.SetTooltipText("")
156 | }
157 | }
158 |
159 | func (item *Item) IsPinned() bool {
160 | return slices.Contains(*item.PinnedList, item.ClassName)
161 | }
162 |
163 | func (item *Item) TogglePin(settings settings.Settings) {
164 |
165 | if item.IsPinned() {
166 | utils.RemoveFromSliceByValue(item.PinnedList, item.ClassName)
167 | if item.Instances == 0 {
168 | item.ButtonBox.Destroy()
169 | delete(item.List, item.ClassName)
170 | }
171 | log.Println("Remove:", item.ClassName)
172 | } else {
173 | utils.AddToSlice(item.PinnedList, item.ClassName)
174 | log.Println("Add:", item.ClassName)
175 | }
176 |
177 | err := cfg.ChangeJsonPinnedApps(*item.PinnedList, settings.PinnedPath)
178 | if err != nil {
179 | log.Println("Error: ", err)
180 | } else {
181 | log.Println("File", settings.PinnedPath, "saved successfully!", item.ClassName)
182 | }
183 | }
184 |
185 | func (item *Item) Remove() {
186 | item.ButtonBox.Destroy()
187 | delete(item.List, item.ClassName)
188 | }
189 |
190 | func appendInducator(parent *gtk.Box, child *gtk.Image, pos string) {
191 | switch pos {
192 | case "left", "right":
193 | buf := child.GetPixbuf()
194 | newBuf, err := buf.RotateSimple(gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE)
195 | if err != nil {
196 | return
197 | }
198 | child.SetFromPixbuf(newBuf)
199 | }
200 |
201 | switch pos {
202 | case "left", "top":
203 | parent.PackStart(child, false, false, 0)
204 | parent.ReorderChild(child, 0)
205 | case "bottom", "right":
206 | parent.PackEnd(child, false, false, 0)
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/internal/pvwidget/pvwidget.go:
--------------------------------------------------------------------------------
1 | package pvwidget
2 |
3 | import (
4 | "fmt"
5 | "hypr-dock/internal/hysc"
6 | "hypr-dock/internal/item"
7 | "hypr-dock/internal/pkg/utils"
8 | "hypr-dock/internal/settings"
9 | "hypr-dock/pkg/ipc"
10 | "log"
11 | "sync"
12 |
13 | "github.com/gotk3/gotk3/gdk"
14 | "github.com/gotk3/gotk3/glib"
15 | "github.com/gotk3/gotk3/gtk"
16 | "github.com/gotk3/gotk3/pango"
17 | )
18 |
19 | func New(item *item.Item, settings settings.Settings, onReady func(w, h int)) (box *gtk.Box, err error) {
20 | padding := settings.PreviewStyle.Padding
21 |
22 | wrapper, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, settings.ContextPos)
23 | if err != nil {
24 | return nil, err
25 | }
26 | wrapper.SetName("pv-wrap")
27 |
28 | var (
29 | totalWidth int
30 | readyCount int
31 | commonHeight int
32 | mutex sync.Mutex
33 | )
34 |
35 | for _, window := range item.Windows {
36 | windowBox, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
37 | if err != nil {
38 | log.Println(err)
39 | continue
40 | }
41 |
42 | windowBox.SetName("pv-item")
43 |
44 | eventBox, err := gtk.EventBoxNew()
45 | if err != nil {
46 | log.Println(err)
47 | continue
48 | }
49 |
50 | eventBox.SetName("pv-event-box")
51 |
52 | windowBoxContent, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
53 | if err != nil {
54 | log.Println(err)
55 | continue
56 | }
57 |
58 | windowBoxContent.SetMarginBottom(padding)
59 | windowBoxContent.SetMarginEnd(padding)
60 | windowBoxContent.SetMarginStart(padding)
61 | windowBoxContent.SetMarginTop(padding / 2)
62 |
63 | titleBox, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
64 | if err != nil {
65 | log.Println(err)
66 | continue
67 | }
68 |
69 | titleBox.SetMarginBottom(padding / 2)
70 |
71 | icon, err := utils.CreateImage(item.DesktopData.Icon, 16)
72 | if err != nil {
73 | log.Println(err)
74 | continue
75 | }
76 |
77 | label, err := gtk.LabelNew(window["Title"])
78 | if err != nil {
79 | log.Println(err)
80 | continue
81 | }
82 |
83 | label.SetEllipsize(pango.ELLIPSIZE_END)
84 | label.SetXAlign(0)
85 | label.SetHExpand(true)
86 | // label.SetMarginBottom(2)
87 |
88 | closeBtn, err := gtk.ButtonNewFromIconName("close", gtk.ICON_SIZE_SMALL_TOOLBAR)
89 | if err != nil {
90 | log.Println(err)
91 | continue
92 | }
93 | closeBtn.SetName("close-btn")
94 | utils.AddStyle(closeBtn, "#close-btn {padding: 0;}")
95 |
96 | eventBox.Connect("button-press-event", func(eb *gtk.EventBox, e *gdk.Event) {
97 | go ipc.Hyprctl("dispatch focuswindow address:" + window["Address"])
98 | go ipc.DispatchEvent("hd>>focus-window")
99 | })
100 |
101 | context, err := windowBox.GetStyleContext()
102 | if err == nil {
103 | eventBox.Connect("enter-notify-event", func() {
104 | context.AddClass("hover")
105 | })
106 | eventBox.Connect("leave-notify-event", func(eb *gtk.EventBox, e *gdk.Event) {
107 | event := gdk.EventCrossingNewFromEvent(e)
108 | isInWindow := event.Detail() == 3 || event.Detail() == 0
109 |
110 | if isInWindow {
111 | context.RemoveClass("hover")
112 | }
113 | })
114 | }
115 |
116 | display, err := gdk.DisplayGetDefault()
117 | if err == nil {
118 | pointer, _ := gdk.CursorNewFromName(display, "pointer")
119 | arrow, _ := gdk.CursorNewFromName(display, "default")
120 |
121 | eventBox.Connect("enter-notify-event", func() {
122 | win, _ := eventBox.GetWindow()
123 | if win != nil {
124 | win.SetCursor(pointer)
125 | }
126 | })
127 |
128 | eventBox.Connect("leave-notify-event", func(eb *gtk.EventBox, e *gdk.Event) {
129 | event := gdk.EventCrossingNewFromEvent(e)
130 | win, _ := eventBox.GetWindow()
131 |
132 | if win != nil && event.Detail() != 2 {
133 | win.SetCursor(arrow)
134 | }
135 | })
136 | }
137 |
138 | stream, err := hysc.StreamNew(window["Address"])
139 | if err != nil {
140 | log.Println(err)
141 | continue
142 | }
143 |
144 | stream.OnReady(func(s *hysc.Size) {
145 | if s == nil {
146 | return
147 | }
148 |
149 | closeBtn.Connect("button-press-event", func() {
150 | go ipc.Hyprctl("dispatch closewindow address:" + window["Address"])
151 | if item.Instances == 1 {
152 | go ipc.DispatchEvent("hd>>focus-window")
153 | return
154 | }
155 |
156 | mutex.Lock()
157 | defer mutex.Unlock()
158 |
159 | totalWidth = totalWidth - s.W - padding*2 - settings.ContextPos
160 |
161 | go ipc.DispatchEvent(fmt.Sprintf("hd>>close-window>>%d", totalWidth))
162 | windowBox.Destroy()
163 | wrapper.ShowAll()
164 | })
165 |
166 | glib.IdleAdd(func() {
167 | mutex.Lock()
168 | defer mutex.Unlock()
169 |
170 | totalWidth += s.W
171 | readyCount++
172 | commonHeight = s.H
173 |
174 | if readyCount == len(item.Windows) {
175 | totalWidth = totalWidth + settings.ContextPos*(len(item.Windows)-1) + 2*padding*len(item.Windows)
176 | commonHeight = commonHeight + 2*padding + 20
177 |
178 | onReady(totalWidth, commonHeight)
179 | }
180 | })
181 | })
182 |
183 | stream.SetHScale(settings.PreviewStyle.Size)
184 | stream.SetBorderRadius(settings.PreviewStyle.BorderRadius)
185 |
186 | if settings.Preview == "live" {
187 | err = stream.Start(settings.PreviewAdvanced.FPS, settings.PreviewAdvanced.BufferSize)
188 | } else {
189 | err = stream.CaptureFrame()
190 | }
191 |
192 | if err != nil {
193 | log.Println(err)
194 | continue
195 | }
196 |
197 | titleBox.Add(icon)
198 | titleBox.Add(label)
199 | titleBox.Add(closeBtn)
200 |
201 | windowBoxContent.Add(titleBox)
202 | windowBoxContent.Add(stream)
203 |
204 | eventBox.Add(windowBoxContent)
205 | windowBox.Add(eventBox)
206 | wrapper.Add(windowBox)
207 | }
208 |
209 | return wrapper, nil
210 | }
211 |
--------------------------------------------------------------------------------
/internal/pvctl/pvctl.go:
--------------------------------------------------------------------------------
1 | package pvctl
2 |
3 | import (
4 | "fmt"
5 | "hypr-dock/internal/item"
6 | layerinfo "hypr-dock/internal/layerInfo"
7 | "hypr-dock/internal/pkg/popup"
8 | "hypr-dock/internal/pkg/timer"
9 | "hypr-dock/internal/pvwidget"
10 | "hypr-dock/internal/settings"
11 | "hypr-dock/pkg/ipc"
12 | "log"
13 | "strconv"
14 | "strings"
15 |
16 | "github.com/gotk3/gotk3/gdk"
17 | "github.com/gotk3/gotk3/glib"
18 | "github.com/gotk3/gotk3/gtk"
19 | )
20 |
21 | type PV struct {
22 | active bool
23 |
24 | showTimer *timer.Timer
25 | hideTimer *timer.Timer
26 | moveTimer *timer.Timer
27 |
28 | className string
29 | popup *popup.Popup
30 | }
31 |
32 | func New(settings settings.Settings) *PV {
33 | return &PV{
34 | className: "90348d332fvecs324csd4",
35 |
36 | showTimer: timer.New(),
37 | hideTimer: timer.New(),
38 | moveTimer: timer.New(),
39 |
40 | popup: popup.New(),
41 | }
42 | }
43 |
44 | func (pv *PV) Show(item *item.Item, settings settings.Settings) {
45 | if pv == nil || item == nil {
46 | fmt.Println("pv is nil:", pv == nil, "|", "item is nil:", item == nil)
47 | return
48 | }
49 |
50 | hide := func() {
51 | glib.IdleAdd(func() {
52 | pv.Hide()
53 | })
54 | pv.SetActive(false)
55 | }
56 |
57 | ipc.AddEventListener("hd>>focus-window", func(e string) {
58 | pv.showTimer.Stop()
59 | hide()
60 | }, true)
61 |
62 | if settings.Position == "top" || settings.Position == "bottom" {
63 | ipc.AddEventListener("hd>>close-window", func(e string) {
64 | eventDetail := strings.TrimPrefix(e, "hd>>close-window>>")
65 | w, _ := strconv.Atoi(eventDetail)
66 | x, y, _ := getCord(item.Button, settings)
67 |
68 | pv.popup.Move(x-w/2, y)
69 | }, true)
70 | }
71 |
72 | pv.popup.SetWinCallBack(func(w *gtk.Window) error {
73 | w.Connect("enter-notify-event", func() {
74 | pv.hideTimer.Stop()
75 | ipc.DispatchEvent("hd>>pv-pointer-enter")
76 | })
77 | w.Connect("leave-notify-event", func(w *gtk.Window, e *gdk.Event) {
78 | event := gdk.EventCrossingNewFromEvent(e)
79 | isInWindow := event.Detail() == 3 || event.Detail() == 4
80 |
81 | if !isInWindow {
82 | return
83 | }
84 | pv.hideTimer.Run(settings.PreviewAdvanced.HideDelay, hide)
85 | ipc.DispatchEvent("hd>>pv-pointer-leave")
86 | })
87 | return nil
88 | })
89 |
90 | widget, err := pvwidget.New(item, settings, func(w, h int) {
91 | setCord(w, h, item, settings, func(x, y int, startx, starty string) {
92 | pv.popup.Open(x, y, startx, starty)
93 | })
94 | })
95 | if err != nil {
96 | log.Println(err)
97 | return
98 | }
99 |
100 | pv.popup.Set(widget)
101 | }
102 |
103 | func (pv *PV) Hide() {
104 | pv.popup.Close()
105 | }
106 |
107 | func (pv *PV) Change(item *item.Item, settings settings.Settings) {
108 | if pv == nil || item == nil {
109 | fmt.Println("pv is nil:", pv == nil, "|", "item is nil:", item == nil)
110 | return
111 | }
112 |
113 | hide := func() {
114 | glib.IdleAdd(func() {
115 | pv.Hide()
116 | })
117 | pv.SetActive(false)
118 | }
119 |
120 | pv.popup.SetWinCallBack(func(w *gtk.Window) error {
121 | w.Connect("enter-notify-event", func() {
122 | pv.hideTimer.Stop()
123 | })
124 | w.Connect("leave-notify-event", func() {
125 | pv.hideTimer.Run(settings.PreviewAdvanced.HideDelay, hide)
126 | })
127 | return nil
128 | })
129 |
130 | widget, err := pvwidget.New(item, settings, func(w, h int) {
131 | setCord(w, h, item, settings, func(x, y int, startx, starty string) {
132 | pv.popup.Move(x, y)
133 | })
134 | })
135 | if err != nil {
136 | log.Println(err)
137 | return
138 | }
139 |
140 | pv.popup.Set(widget)
141 | }
142 |
143 | func (pv *PV) SetActive(flag bool) {
144 | pv.active = flag
145 | }
146 |
147 | func (pv *PV) GetActive() bool {
148 | return pv.active
149 | }
150 |
151 | func (pv *PV) GetShowTimer() *timer.Timer {
152 | return pv.showTimer
153 | }
154 |
155 | func (pv *PV) GetHideTimer() *timer.Timer {
156 | return pv.hideTimer
157 | }
158 |
159 | func (pv *PV) GetMoveTimer() *timer.Timer {
160 | return pv.moveTimer
161 | }
162 |
163 | func (pv *PV) HasClassChanged(className string) bool {
164 | return pv.className != className
165 | }
166 |
167 | func (pv *PV) SetCurrentClass(className string) {
168 | pv.className = className
169 | }
170 |
171 | func getCord(v *gtk.Button, settings settings.Settings) (int, int, error) {
172 | margin := settings.ContextPos
173 | pos := settings.Position
174 |
175 | dock, err := layerinfo.GetDock()
176 | if err != nil {
177 | log.Println(err)
178 | }
179 |
180 | var x int
181 | var y int
182 |
183 | switch pos {
184 | case "bottom":
185 | x = dock.X + v.GetAllocation().GetX() + v.GetAllocatedWidth()/2
186 | y = margin
187 | case "left":
188 | x = margin
189 | y = dock.Y + v.GetAllocation().GetY() + v.GetAllocatedHeight()/2
190 | case "top":
191 | x = dock.X + v.GetAllocation().GetX() + v.GetAllocatedWidth()/2
192 | y = margin
193 | case "right":
194 | x = margin
195 | y = dock.Y + v.GetAllocation().GetY() + v.GetAllocatedHeight()/2
196 | }
197 |
198 | switch pos {
199 | case "bottom", "top":
200 | y = y + dock.H
201 | case "left", "right":
202 | x = x + dock.W
203 | }
204 |
205 | return x, y, err
206 | }
207 |
208 | func setCord(w, h int, item *item.Item, settings settings.Settings, callBack func(x, y int, startx, starty string)) {
209 | x, y, _ := getCord(item.Button, settings)
210 | var startx, starty string
211 |
212 | switch settings.Position {
213 | case "bottom":
214 | startx = "left"
215 | starty = "bottom"
216 | case "right":
217 | startx = "right"
218 | starty = "top"
219 | case "left":
220 | startx = "left"
221 | starty = "top"
222 | case "top":
223 | startx = "left"
224 | starty = "top"
225 | }
226 |
227 | switch settings.Position {
228 | case "top", "bottom":
229 | x = x - w/2
230 | case "left", "right":
231 | y = y - h/2
232 | }
233 |
234 | callBack(x, y, startx, starty)
235 | }
236 |
--------------------------------------------------------------------------------
/internal/pkg/cfg/cfg.go:
--------------------------------------------------------------------------------
1 | package cfg
2 |
3 | import (
4 | "hypr-dock/internal/pkg/utils"
5 | "hypr-dock/internal/pkg/validate"
6 | "log"
7 | "os"
8 |
9 | "github.com/goccy/go-json"
10 | "github.com/pkg/errors"
11 | "github.com/tailscale/hujson"
12 | )
13 |
14 | type Config struct {
15 | CurrentTheme string
16 | IconSize int
17 | Layer string
18 | Position string
19 | Blur string
20 | Spacing int
21 | AutoHideDeley int
22 | SystemGapUsed string
23 | Margin int
24 | ContextPos int
25 | Preview string
26 | PreviewAdvanced struct {
27 | FPS int
28 | BufferSize int
29 | ShowDelay int
30 | HideDelay int
31 | MoveDelay int
32 | }
33 | PreviewStyle struct {
34 | Size int
35 | BorderRadius int
36 | Padding int
37 | }
38 | }
39 |
40 | type ThemeConfig struct {
41 | Blur string
42 | Spacing int
43 | PreviewStyle struct {
44 | Size int
45 | BorderRadius int
46 | Padding int
47 | }
48 | }
49 |
50 | type ItemList struct {
51 | Pinned []string
52 | }
53 |
54 | func GetDefaultConfig() Config {
55 | return Config{
56 | CurrentTheme: "lotos",
57 | IconSize: 21,
58 | Layer: "auto",
59 | Position: "bottom",
60 | Blur: "true",
61 | Spacing: 8,
62 | SystemGapUsed: "true",
63 | AutoHideDeley: 400,
64 | Margin: 8,
65 | ContextPos: 5,
66 | Preview: "none",
67 | PreviewAdvanced: struct {
68 | FPS int
69 | BufferSize int
70 | ShowDelay int
71 | HideDelay int
72 | MoveDelay int
73 | }{
74 | FPS: 30,
75 | BufferSize: 5,
76 | ShowDelay: 600,
77 | HideDelay: 300,
78 | MoveDelay: 200,
79 | },
80 | PreviewStyle: struct {
81 | Size int
82 | BorderRadius int
83 | Padding int
84 | }{
85 | Size: 120,
86 | BorderRadius: 0,
87 | Padding: 10,
88 | },
89 | }
90 | }
91 |
92 | func ReadConfig(jsoncFile string, themesDir string) Config {
93 | // Read jsonc
94 | config := Config{}
95 | err := ReadJsonc(jsoncFile, &config)
96 | if err != nil {
97 | log.Println(err)
98 | log.Println("Load default config")
99 | return GetDefaultConfig()
100 | }
101 |
102 | // Set default values if not specified
103 | if config.CurrentTheme == "" {
104 | config.CurrentTheme = GetDefaultConfig().CurrentTheme
105 | log.Println("The theme is not set, the default theme is currently used - \"lotos\"")
106 | }
107 |
108 | if !validate.Layer(config.Layer, false) {
109 | config.Layer = GetDefaultConfig().Layer
110 | }
111 |
112 | if !validate.Position(config.Position, false) {
113 | config.Position = GetDefaultConfig().Position
114 | }
115 |
116 | if !validate.Blur(config.Blur, false) {
117 | config.Blur = GetDefaultConfig().Blur
118 | }
119 |
120 | if !validate.SystemGapUsed(config.SystemGapUsed, false) {
121 | config.SystemGapUsed = GetDefaultConfig().SystemGapUsed
122 | }
123 |
124 | if !validate.Preview(config.Preview, false) {
125 | config.Preview = GetDefaultConfig().Preview
126 | }
127 |
128 | if config.PreviewAdvanced.FPS == 0 {
129 | config.PreviewAdvanced.FPS = GetDefaultConfig().PreviewAdvanced.FPS
130 | }
131 |
132 | if config.PreviewAdvanced.BufferSize < 1 || config.PreviewAdvanced.BufferSize > 20 {
133 | config.PreviewAdvanced.BufferSize = GetDefaultConfig().PreviewAdvanced.BufferSize
134 | }
135 |
136 | if config.Spacing < 1 {
137 | config.Spacing = GetDefaultConfig().Spacing
138 | }
139 |
140 | if config.IconSize < 1 {
141 | config.IconSize = GetDefaultConfig().IconSize
142 | }
143 |
144 | return config
145 | }
146 |
147 | func ReadTheme(jsoncFile string, config Config) *ThemeConfig {
148 | // Read jsonc
149 | themeConfig := ThemeConfig{}
150 | err := ReadJsonc(jsoncFile, &themeConfig)
151 | if err != nil {
152 | log.Println(err)
153 | log.Println("Load default config")
154 | return nil
155 | }
156 |
157 | // Set default values if not specified
158 | if !validate.Blur(config.Blur, false) {
159 | themeConfig.Blur = config.Blur
160 | }
161 |
162 | if themeConfig.Spacing < 0 {
163 | themeConfig.Spacing = config.Spacing
164 | }
165 |
166 | if themeConfig.PreviewStyle.Size < 20 {
167 | themeConfig.PreviewStyle.Size = GetDefaultConfig().PreviewStyle.Size
168 | }
169 |
170 | return &themeConfig
171 | }
172 |
173 | func ReadItemList(jsonFile string) []string {
174 | itemList := ItemList{}
175 |
176 | if !utils.FileExists(jsonFile) {
177 | itemList.Pinned = CreateEmptyPinnedFile(jsonFile)
178 | return itemList.Pinned
179 | }
180 |
181 | err := ReadJson(jsonFile, &itemList)
182 | if err != nil {
183 | log.Fatal(err)
184 | }
185 |
186 | return itemList.Pinned
187 | }
188 |
189 | func ReadJsonc(jsoncFile string, v interface{}) error {
190 | file, err := os.ReadFile(jsoncFile)
191 | if err != nil {
192 | return errors.Wrapf(err, "file %q not found", jsoncFile)
193 | }
194 |
195 | // Парсим JSONC
196 | standardized, err := hujson.Standardize(file)
197 | if err != nil {
198 | return errors.Wrapf(err, "failed to standardize JSONC")
199 | }
200 |
201 | if err := json.Unmarshal(standardized, &v); err != nil {
202 | return errors.Wrapf(err, "file %q has a syntax error", jsoncFile)
203 | }
204 |
205 | return nil
206 | }
207 |
208 | func ChangeJsonPinnedApps(apps []string, jsonFile string) error {
209 | itemList := ItemList{
210 | Pinned: apps,
211 | }
212 |
213 | if err := WriteItemList(jsonFile, itemList); err != nil {
214 | log.Println("Error", jsonFile, "writing: ", err)
215 | return err
216 | }
217 |
218 | return nil
219 | }
220 |
221 | func ReadJson(jsonFile string, v interface{}) error {
222 | file, err := os.Open(jsonFile)
223 | if err != nil {
224 | return err
225 | }
226 | defer file.Close()
227 |
228 | decoder := json.NewDecoder(file)
229 | if err := decoder.Decode(&v); err != nil {
230 | return err
231 | }
232 |
233 | return nil
234 | }
235 |
236 | func CreateEmptyPinnedFile(jsonFile string) []string {
237 | initialData := ItemList{
238 | Pinned: []string{},
239 | }
240 |
241 | if err := WriteItemList(jsonFile, initialData); err != nil {
242 | log.Fatalf("Failed to create file %q: %v", jsonFile, err)
243 | return nil
244 | }
245 |
246 | return initialData.Pinned
247 | }
248 |
249 | func WriteItemList(jsonFile string, data ItemList) error {
250 | file, err := os.Create(jsonFile)
251 | if err != nil {
252 | return errors.Wrapf(err, "failed to create file %q", jsonFile)
253 | }
254 | defer file.Close()
255 |
256 | encoder := json.NewEncoder(file)
257 | encoder.SetIndent("", " ")
258 | if err := encoder.Encode(data); err != nil {
259 | return errors.Wrapf(err, "failed to encode data to file %q", jsonFile)
260 | }
261 |
262 | return nil
263 | }
264 |
--------------------------------------------------------------------------------
/docs/tasks/cli/hypr-dock-ctl-example.md:
--------------------------------------------------------------------------------
1 | Давай разберем концептуальный состав, функционал и примерные концепции реализации для cli утилиты. Утилита будет называться hypr-dock-ctl. Ее главная и единственная задача это отправлять команды в сокет собирая их из аргументов вызова. Например запрос вида hypr-dock-ctl layer get —json утилита превращает в строку j/layer get; запрос hypr-dock-ctl layer set <данные которые передаются следующими аргументами> превращает в строку “layer set данные которые передаются следующими аргументами”. Эти строки должны передаваться в сокет и получать ответ сервера. Если произошла ошибка код завершения берётся из ответа сервера, если ошибок нет, то код 0
2 |
3 | # **Концепция утилиты hypr-dock-ctl**
4 |
5 | ## **1. Назначение и функционал**
6 | Утилита `hypr-dock-ctl` служит универсальным клиентом для взаимодействия с демоном через IPC-сокет. Её ключевые функции:
7 |
8 | 1. **Трансляция команд**
9 | Преобразует аргументы CLI в строку для сокета:
10 | ```
11 | hypr-dock-ctl <команда> <действие> [данные] [--json]
12 | →
13 | j/<команда> <действие> [данные] # с --json
14 | <команда> <действие> [данные] # без --json
15 | ```
16 |
17 | 2. **Передача запросов**
18 | Отправляет сформированную строку в Unix-domain сокет (`/tmp/hypr-dock.sock`).
19 |
20 | 3. **Обработка ответов**
21 | - Выводит сырой ответ сервера (текст/JSON)
22 | - Возвращает код завершения:
23 | - `0` при успехе
24 | - Код ошибки из ответа (если есть)
25 | - `1` при ошибках соединения
26 |
27 | ---
28 |
29 | ## **2. Формат команд**
30 |
31 | ### **2.1 Структура вызова**
32 | ```
33 | hypr-dock-ctl [data...] [--json]
34 | ```
35 |
36 | | Часть | Описание | Обязательность |
37 | |------------|-----------------------------------|----------------|
38 | | `command` | Основная команда (например `layer`) | Да |
39 | | `action` | Действие (`get`, `set`, `list`) | Да |
40 | | `data` | Аргументы через пробел | Нет |
41 | | `--json` | Флаг JSON-формата | Нет |
42 |
43 | ### **2.2 Примеры**
44 | ```bash
45 | # Текстовый запрос
46 | hypr-dock-ctl layer get
47 |
48 | # JSON-запрос
49 | hypr-dock-ctl window list --json
50 |
51 | # Команда с данными
52 | hypr-dock-ctl config set theme dark
53 |
54 | # Команда с составными данными
55 | hypr-dock-ctl notification create "Hello" "Message text"
56 | ```
57 |
58 | ---
59 |
60 | ## **3. Обработка ответов**
61 |
62 | ### **3.1 Успешный ответ**
63 | - **Текстовый режим**: выводит данные как есть
64 | ```
65 | workspace: main
66 | windows: 3
67 | ```
68 | - **JSON-режим**: выводит сырой JSON
69 | ```json
70 | {"workspace":"main","windows":3}
71 | ```
72 | - **Код завершения**: `0`
73 |
74 | ### **3.2 Ошибка**
75 | - **Текстовый режим**:
76 | ```
77 | error: Invalid command [2]
78 | ```
79 | - **JSON-режим**:
80 | ```json
81 | {"error":"Invalid command","code":2}
82 | ```
83 | - **Код завершения**: извлекается из ответа (`[2]` или `"code":2`)
84 |
85 | ### **3.3 Ошибки соединения**
86 | - Вывод: `error: Connection failed`
87 | - Код: `1`
88 |
89 | ---
90 |
91 | ## **4. Концепция реализации (Go)**
92 |
93 | ### **4.1 Основные компоненты**
94 | ```go
95 | package main
96 |
97 | import (
98 | "fmt"
99 | "net"
100 | "os"
101 | "strings"
102 | )
103 |
104 | func main() {
105 | // 1. Парсинг аргументов
106 | args, useJSON := parseArgs(os.Args[1:])
107 |
108 | // 2. Формирование запроса
109 | request := buildRequest(args, useJSON)
110 |
111 | // 3. Отправка в сокет
112 | response, err := sendToSocket(request, "/tmp/hypr-dock.sock")
113 | if err != nil {
114 | fmt.Println("error: Connection failed")
115 | os.Exit(1)
116 | }
117 |
118 | // 4. Обработка ответа
119 | exitCode := handleResponse(response, useJSON)
120 | os.Exit(exitCode)
121 | }
122 | ```
123 |
124 | ### **4.2 Ключевые функции**
125 | 1. **Парсинг аргументов**:
126 | ```go
127 | func parseArgs(args []string) (cmdParts []string, useJSON bool) {
128 | for i, arg := range args {
129 | if arg == "--json" {
130 | return append(args[:i], args[i+1:]...), true
131 | }
132 | }
133 | return args, false
134 | }
135 | ```
136 |
137 | 2. **Построение запроса**:
138 | ```go
139 | func buildRequest(parts []string, jsonFlag bool) string {
140 | req := strings.Join(parts, " ")
141 | if jsonFlag {
142 | return "j/" + req
143 | }
144 | return req
145 | }
146 | ```
147 |
148 | 3. **Отправка в сокет**:
149 | ```go
150 | func sendToSocket(request, socketPath string) (string, error) {
151 | conn, err := net.Dial("unix", socketPath)
152 | if err != nil {
153 | return "", err
154 | }
155 | defer conn.Close()
156 |
157 | _, err = conn.Write([]byte(request + "\n"))
158 | if err != nil {
159 | return "", err
160 | }
161 |
162 | buf := make([]byte, 4096)
163 | n, err := conn.Read(buf)
164 | return string(buf[:n]), err
165 | }
166 | ```
167 |
168 | 4. **Обработка ответа**:
169 | ```go
170 | func handleResponse(response string, isJSON bool) int {
171 | if isJSON {
172 | var resp struct{ Code int `json:"code"` }
173 | if json.Unmarshal([]byte(response), &resp) == nil && resp.Code != 0 {
174 | fmt.Println(response)
175 | return resp.Code
176 | }
177 | } else if strings.HasPrefix(response, "error: ") {
178 | parts := strings.Split(response, "[")
179 | if len(parts) > 1 {
180 | if code, err := strconv.Atoi(strings.TrimSuffix(parts[1], "]")); err == nil {
181 | fmt.Println(response)
182 | return code
183 | }
184 | }
185 | }
186 |
187 | fmt.Print(response)
188 | return 0
189 | }
190 | ```
191 |
192 | ---
193 |
194 | ## **5. Примеры работы**
195 |
196 | ### **Успешный запрос**
197 | ```bash
198 | $ hypr-dock-ctl workspace get
199 | workspace: main
200 | active: true
201 |
202 | $ echo $?
203 | 0
204 | ```
205 |
206 | ### **Ошибка**
207 | ```bash
208 | $ hypr-dock-ctl invalid cmd
209 | error: Unknown command [2]
210 |
211 | $ echo $?
212 | 2
213 | ```
214 |
215 | ### **JSON-запрос**
216 | ```bash
217 | $ hypr-dock-ctl window list --json
218 | {"windows":["term","browser"]}
219 |
220 | $ echo $?
221 | 0
222 | ```
223 |
224 | ---
225 |
226 | ## **6. Преимущества архитектуры**
227 | 1. **Минимализм**: делает ровно одну задачу (передача команд)
228 | 2. **Гибкость**: поддерживает любые команды и данные
229 | 3. **Совместимость**:
230 | - Работает с существующими bash-скриптами
231 | - Поддерживает JSON для сложных клиентов
232 | 4. **Прозрачность**: выводит сырой ответ сервера без изменений
233 |
234 | Такой дизайн соответствует Unix-философии "делать одну вещь и делать её хорошо".
--------------------------------------------------------------------------------
/README_RU.md:
--------------------------------------------------------------------------------
1 | # hypr-dock
2 | ### Интерактивная док-панель для Hyprland
3 |
4 |
5 |
6 |
7 | [](https://youtu.be/HHUZWHfNAl0?si=ZrRv2ggnPBEBS5oY)
8 | [](https://aur.archlinux.org/packages/hypr-dock)
9 |
10 | ## Установка
11 |
12 | ### Зависимости
13 |
14 | - `go` (make)
15 | - `gtk3`
16 | - `gtk-layer-shell`
17 |
18 | ### Установка
19 | ```bash
20 | git clone https://github.com/lotos-linux/hypr-dock.git
21 | cd hypr-dock
22 | make get
23 | make build
24 | make install
25 | ```
26 |
27 | ### Удаление
28 | ```bash
29 | make uninstall
30 | ```
31 |
32 | ### Обновление
33 | ```bash
34 | make update
35 | ```
36 |
37 | ### Локальный запуск (dev mode)
38 | ```bash
39 | make exec
40 | ```
41 |
42 | ## Запуск
43 |
44 | ### Параметры запуска:
45 |
46 | ```text
47 | -config string
48 | config file (default "~/.config/hypr-dock")
49 | -dev
50 | enable developer mode
51 | -theme string
52 | theme dir (default "lotos")
53 | ```
54 | #### Все параметры являются необязательными.
55 |
56 | Конфигурация и темы по умолчания ставяться в `~/.config/hypr-dock`
57 | ### Добавьте запуск в `hyprland.conf`:
58 |
59 | ```text
60 | exec-once = hypr-dock
61 | bind = Super, D, exec, hypr-dock
62 | ```
63 |
64 | #### Док поддерживает только один запущенный экземпляр, так что повторный запуск закроет предыдующий.
65 |
66 | ## Настройка
67 |
68 | ### В `config.jsonc` доступны такие параметры
69 |
70 | ```jsonc
71 | {
72 | "CurrentTheme": "lotos",
73 |
74 | // Icon size (px) (default 23)
75 | "IconSize": 23,
76 |
77 | // Window overlay layer height (auto, exclusive-top, exclusive-bottom, background, bottom, top, overlay) (default "auto")
78 | "Layer": "auto",
79 |
80 | // Window position on screen (top, bottom, left, right) (default "bottom")
81 | "Position": "bottom",
82 |
83 | // Delay before hiding the dock (ms) (default 400)
84 | "AutoHideDeley": 400, // *Only for "Layer": "auto"*
85 |
86 | // Use system gap (true, false) (default "true")
87 | "SystemGapUsed": "true",
88 |
89 | // Indent from the edge of the screen (px) (default 8)
90 | "Margin": 8,
91 |
92 | // Distance of the context menu from the window (px) (default 0)
93 | "ContextPos": 5,
94 |
95 |
96 | // Window thumbnail mode selection (none, live, static) (default "none")
97 | "Preview": "none",
98 | /*
99 | "none" - disabled (text menus)
100 | "static" - last window frame (stable)
101 | "live" - window streaming (unstable) !EXPEREMENTAL!
102 |
103 | !WARNING!
104 | BY SETTING "Preview" TO "live" OR "static", YOU AGREE TO THE CAPTURE
105 | OF WINDOW CONTENTS.
106 | THE "HYPR-DOCK" PROGRAM DOES NOT COLLECT, STORE, OR TRANSMIT ANY DATA.
107 | WINDOW CAPTURE OCCURS ONLY FOR THE DURATION OF THE THUMBNAIL DISPLAY!
108 |
109 | Source code: https://github.com/lotos-linux/hypr-dock
110 | */
111 |
112 | "PreviewAdvanced": {
113 | // Live preview fps (0 - ∞) (default 30)
114 | "FPS": 30,
115 |
116 | // Live preview bufferSize (1 - 20) (default 5)
117 | "BufferSize": 5,
118 |
119 | // Popup show/hide/move delays (ms)
120 | "ShowDelay": 600, // (default 600)
121 | "HideDelay": 300, // (default 300)
122 | "MoveDelay": 200 // (default 200)
123 | }
124 | }
125 | ```
126 | #### Если параметр не указан значение будет выставлено по умолчанию
127 |
128 | ## Разберем неочевидные параметры
129 | ### Layer
130 | - При `"Layer": "auto"` слой дока находиться под всеми окнами, но если увести курсор мыши к краю экрана - док поднимается над ними
131 | - При `"Layer": "exclusive-top"` включается эксклюзивный режим на верхнем слое. Тайлинговые и плавающие окна не будут перекрывать док.
132 | - При `"Layer": "exclusive-bottom"` включается эксклюзивный режим на нижнем слое. Тайлинговые окна не будут перекрывать док. Плавающие окна будут поверх дока.
133 |
134 | ### SystemGapUsed
135 | - При `"SystemGapUsed": "true"` док будет задавать для себя отступ от края экрана беря значение из конфигурации `hyprland`, а конкретно значения `general:gaps_out`, при этом док динамически будет подхватывать изменение конфигурации `hyprland`
136 | - При `"SystemGapUsed": "false"` отступ от края экрана будет задаваться параметром `Margin`
137 |
138 | ### PreviewAdvanced
139 | - `ShowDelay`, `HideDelay`, `MoveDelay` - задержки действий попапа превью в милисекундах
140 | - `FPS`, `BufferSize` - используются только при `"Preview":"live"`
141 |
142 | > Внимание!
143 | > Живое превью ведет себя не стабильно.
144 | > Пока что не рекомендую ставить значение `"Preview": "live"`
145 |
146 |
147 | #### Настройки внешнего вида превью происхрдит через файлы темы
148 |
149 |
150 |
151 | ### Также есть файл `pinned.json` с закрепленными приложениями
152 | #### Например
153 | ```json
154 | {
155 | "Pinned": [
156 | "firefox",
157 | "org.telegram.desktop",
158 | "code-oss",
159 | "kitty"
160 | ]
161 | }
162 | ```
163 | Вы можете менять его в ручную. Но зачем? ¯\_(ツ)_/¯
164 |
165 | ## Темы
166 |
167 | #### Темы находяться в папке `~/.config/hypr-dock/themes/`
168 |
169 | ### Тема состоит из
170 | - `[название_темы].jsonc` например `lotos.jsonc`
171 | - `style.css`
172 | - Папка с `svg` файлами для индикации количества запущенных приложения (смотрите [themes_RU.md](https://github.com/lotos-linux/hypr-dock/blob/main/docs/customize/themes_RU.md))
173 |
174 | ### Конфиг темы
175 | ```jsonc
176 | {
177 | // Blur window ("true", "false") (default "on")
178 | "Blur": "true",
179 |
180 | // Distance between elements (px) (default 9)
181 | "Spacing": 5,
182 |
183 | // Preview settings
184 | "PreviewStyle": {
185 | // Size (px) (default 120)
186 | "Size": 120,
187 |
188 | // Image/Stream border-radius (px) (default 0)
189 | "BorderRadius": 0,
190 |
191 | // Popup padding (px) (default 10)
192 | "Padding": 10
193 | }
194 | }
195 | ```
196 | #### Файл `style.css` крутите как хотите
197 |
198 | ## Использованные библиотки
199 | - [github.com/akshaybharambe14/go-jsonc](https://github.com/akshaybharambe14/go-jsonc) v1.0.0
200 | - [github.com/allan-simon/go-singleinstance](https://github.com/allan-simon/go-singleinstance) v0.0.0-20210120080615-d0997106ab37
201 | - [github.com/dlasky/gotk3-layershell](https://github.com/dlasky/gotk3-layershell) v0.0.0-20240515133811-5c5115f0d774
202 | - [github.com/goccy/go-json](https://github.com/goccy/go-json) v0.10.3
203 | - [github.com/gotk3/gotk3](https://github.com/gotk3/gotk3) v0.6.3
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hypr-dock
2 |
3 | ### Interactive Dock Panel for Hyprland
4 |
5 | Translations: [`Русский`](https://github.com/lotos-linux/hypr-dock/blob/main/README_RU.md)
6 |
7 |
8 |
9 |
10 | [](https://youtu.be/HHUZWHfNAl0?si=ZrRv2ggnPBEBS5oY)
11 | [](https://aur.archlinux.org/packages/hypr-dock)
12 |
13 | ## Installation
14 |
15 | ### Dependencies
16 |
17 | - go (make)
18 | - gtk3
19 | - gtk-layer-shell
20 |
21 | ### Install
22 | ```bash
23 | git clone https://github.com/lotos-linux/hypr-dock.git
24 | cd hypr-dock
25 | make get
26 | make build
27 | make install
28 | ```
29 |
30 | ### Uninstall
31 | ```bash
32 | make uninstall
33 | ```
34 |
35 | ### Update
36 | ```bash
37 | make update
38 | ```
39 |
40 | ### Local run (dev mode)
41 | ```bash
42 | make exec
43 | ```
44 |
45 | ## Launching
46 |
47 | ### Launch Parameters:
48 | ```text
49 | -config string
50 | config file (default "~/.config/hypr-dock")
51 | -dev
52 | enable developer mode
53 | -theme string
54 | theme dir (default "lotos")
55 | ```
56 | #### All parameters are optional.
57 |
58 | The default configuration and themes are installed in `~/.config/hypr-dock`
59 |
60 | ### Add the following to hyprland.conf for autostart:
61 | ```text
62 | exec-once = hypr-dock
63 | bind = Super, D, exec, hypr-dock
64 | ```
65 |
66 | #### The dock supports only one running instance, so launching it again will close the previous instance.
67 |
68 | ## Configuration
69 |
70 | ### The following parameters are available in config.jsonc:
71 | ```jsonc
72 | {
73 | "CurrentTheme": "lotos",
74 |
75 | // Icon size (px) (default 23)
76 | "IconSize": 23,
77 |
78 | // Window overlay layer height (auto, exclusive-top, exclusive-bottom, background, bottom, top, overlay) (default "auto")
79 | "Layer": "auto",
80 |
81 | // Window position on screen (top, bottom, left, right) (default "bottom")
82 | "Position": "bottom",
83 |
84 | // Delay before hiding the dock (ms) (default 400)
85 | "AutoHideDeley": 400, // *Only for "Layer": "auto"*
86 |
87 | // Use system gap (true, false) (default "true")
88 | "SystemGapUsed": "true",
89 |
90 | // Indent from the edge of the screen (px) (default 8)
91 | "Margin": 8,
92 |
93 | // Distance of the context menu from the window (px) (default 0)
94 | "ContextPos": 5,
95 |
96 |
97 | // Window thumbnail mode selection (none, live, static) (default "none")
98 | "Preview": "static",
99 | /*
100 | "none" - disabled (text menus)
101 | "static" - last window frame (stable)
102 | "live" - window streaming (unstable) !EXPEREMENTAL!
103 |
104 | !WARNING!
105 | BY SETTING "Preview" TO "live" OR "static", YOU AGREE TO THE CAPTURE
106 | OF WINDOW CONTENTS.
107 | THE "HYPR-DOCK" PROGRAM DOES NOT COLLECT, STORE, OR TRANSMIT ANY DATA.
108 | WINDOW CAPTURE OCCURS ONLY FOR THE DURATION OF THE THUMBNAIL DISPLAY!
109 |
110 | Source code: https://github.com/lotos-linux/hypr-dock
111 | */
112 |
113 | "PreviewAdvanced": {
114 | // Live preview fps (0 - ∞) (default 30)
115 | "FPS": 30,
116 |
117 | // Live preview bufferSize (1 - 20) (default 5)
118 | "BufferSize": 5,
119 |
120 | // Popup show/hide/move delays (ms)
121 | "ShowDelay": 600, // (default 600)
122 | "HideDelay": 300, // (default 300)
123 | "MoveDelay": 200 // (default 200)
124 | }
125 | }
126 | ```
127 | #### If a parameter is not specified, the default value will be used.
128 |
129 | ## Explanation of Non-Obvious Parameters
130 | ### Layer
131 | - With `"Layer": "auto"` the dock layer is below all windows, but if you move the mouse cursor to the edge of the screen, the dock rises above them.
132 | - With `"Layer": "exclusive-top"` - exclusive mode is enabled on the top layer. Neither tiled nor floating windows will overlap the dock.
133 | - With `"Layer": "exclusive-bottom"` - exclusive mode is enabled on the bottom layer. Tiled windows won't overlap the dock. Floating windows will appear above the dock.
134 | ### SystemGapUsed
135 | - With `"SystemGapUsed": "true"` the dock will set its margin from the edge of the screen based on the hyprland configuration, specifically the `general:gaps_out` value. The dock will dynamically adapt to changes in the hyprland configuration.
136 | - With `"SystemGapUsed": "false"` the margin from the edge of the screen will be set by the `Margin` parameter.
137 |
138 | ### PreviewAdvanced
139 | - `ShowDelay`, `HideDelay`, `MoveDelay` - delays for preview popup actions in milliseconds.
140 | - `FPS`, `BufferSize` - only used with `"Preview":"live"`
141 |
142 | > Warning!
143 | > Live preview behaves unpredictably.
144 | > Currently, it is not recommended to set `"Preview": "live"`
145 |
146 | #### Настройки внешнего вида превью происхрдит через файлы темы
147 |
148 | ### There is also a pinned.json file for pinned applications
149 | #### Example:
150 | ```json
151 | {
152 | "Pinned": [
153 | "firefox",
154 | "org.telegram.desktop",
155 | "code-oss",
156 | "kitty"
157 | ]
158 | }
159 | ```
160 | You can edit it manually. But why? ¯\_(ツ)_/¯
161 |
162 | ## Themes
163 |
164 | #### Themes are located in the `~/.config/hypr-dock/themes/` folder
165 |
166 | ### A theme consists of:
167 | - `[theme_name].jsonc`, for example `lotos.jsonc`
168 | - `style.css`
169 | - A folder with `svg` files for indicating the number of running applications (more [themes.md](https://github.com/lotos-linux/hypr-dock/blob/main/docs/customize/themes.md))
170 |
171 | ### Theme Config
172 | ```jsonc
173 | {
174 | // Blur window ("true", "false") (default "on")
175 | "Blur": "true",
176 |
177 | // Distance between elements (px) (default 9)
178 | "Spacing": 5,
179 |
180 | // Preview settings
181 | "PreviewStyle": {
182 | // Size (px) (default 120)
183 | "Size": 120,
184 |
185 | // Image/Stream border-radius (px) (default 0)
186 | "BorderRadius": 0,
187 |
188 | // Popup padding (px) (default 10)
189 | "Padding": 10
190 | }
191 | }
192 | ```
193 | #### Feel free to customize the style.css file as you like.
194 |
195 | ## Libraries Used
196 | - [github.com/akshaybharambe14/go-jsonc](https://github.com/akshaybharambe14/go-jsonc) v1.0.0
197 | - [github.com/allan-simon/go-singleinstance](https://github.com/allan-simon/go-singleinstance) v0.0.0-20210120080615-d0997106ab37
198 | - [github.com/dlasky/gotk3-layershell](https://github.com/dlasky/gotk3-layershell) v0.0.0-20240515133811-5c5115f0d774
199 | - [github.com/goccy/go-json](https://github.com/goccy/go-json) v0.10.3
200 | - [github.com/gotk3/gotk3](https://github.com/gotk3/gotk3) v0.6.3
201 |
--------------------------------------------------------------------------------
/internal/hysc/hysc.go:
--------------------------------------------------------------------------------
1 | package hysc
2 |
3 | import (
4 | "fmt"
5 | "hypr-dock/pkg/wl"
6 | "log"
7 |
8 | "github.com/gotk3/gotk3/gdk"
9 | "github.com/gotk3/gotk3/glib"
10 | "github.com/gotk3/gotk3/gtk"
11 | "github.com/hashicorp/go-hclog"
12 | )
13 |
14 | type Stream struct {
15 | address string
16 | handle uint64
17 |
18 | size *Size
19 | scaleMode *ScaleMode
20 | interpType gdk.InterpType
21 |
22 | effects map[string]func(p *gdk.Pixbuf) error
23 | masks map[string]func(p *gdk.Pixbuf) error
24 |
25 | readyHandler func(*Size)
26 | frameHandler func(*Size)
27 | errorHandler func(error)
28 |
29 | *gtk.Image
30 | }
31 |
32 | type ScaleMode struct {
33 | scaleW int
34 | scaleH int
35 | scaleF float64
36 | }
37 |
38 | type Size struct {
39 | W, H int
40 | }
41 |
42 | type Cord struct {
43 | X, Y int
44 | }
45 |
46 | func StreamAndStart(address string, fps int) (*Stream, error) {
47 | s, err := StreamNew(address)
48 | if err != nil {
49 | return nil, err
50 | }
51 |
52 | err = s.Start(fps)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | return s, nil
58 | }
59 |
60 | func StreamNew(address string) (*Stream, error) {
61 | handle, err := getHandle(address)
62 | if err != nil {
63 | return nil, err
64 | }
65 |
66 | widget, err := gtk.ImageNew()
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | return &Stream{
72 | address: address,
73 | handle: handle,
74 |
75 | size: nil,
76 | scaleMode: nil,
77 | interpType: gdk.INTERP_BILINEAR,
78 |
79 | effects: make(map[string]func(p *gdk.Pixbuf) error, 0),
80 | masks: make(map[string]func(p *gdk.Pixbuf) error, 0),
81 |
82 | readyHandler: nil,
83 | frameHandler: nil,
84 | errorHandler: func(err error) {
85 | log.Println(err)
86 | },
87 |
88 | Image: widget,
89 | }, nil
90 | }
91 |
92 | func (s *Stream) Start(fps int, buferSize ...int) error {
93 | app, err := wl.NewApp(hclog.Default())
94 | if err != nil {
95 | return fmt.Errorf("failed to wayland connection: %v", err)
96 | }
97 |
98 | var bufSize int
99 | if len(buferSize) == 0 {
100 | bufSize = 5
101 | } else {
102 | bufSize = buferSize[0]
103 | }
104 |
105 | stream, err := app.StartStream(s.handle, fps, bufSize)
106 | if err != nil {
107 | app.Close()
108 | return fmt.Errorf("failed to start stream: %v", err)
109 | }
110 |
111 | go func() {
112 | for frame := range stream.Frames {
113 | glib.IdleAdd(func() {
114 | pixbuf, err := nRGBAtoPixbuf(frame)
115 | if err != nil {
116 | s.errorHandler(fmt.Errorf("failed to convert pixbuf: %v", err))
117 | return
118 | }
119 |
120 | if s == nil {
121 | app.Close()
122 | return
123 | }
124 |
125 | if s.scaleMode != nil {
126 | pixbuf, err = s.scale(pixbuf)
127 | if err != nil {
128 | s.errorHandler(fmt.Errorf("failed to scale pixbuf: %v", err))
129 | return
130 | }
131 | }
132 |
133 | for id, effect := range s.effects {
134 | err = effect(pixbuf)
135 | if err != nil {
136 | s.errorHandler(fmt.Errorf("'%s' effect error: %v", id, err))
137 | return
138 | }
139 | }
140 |
141 | for id, mask := range s.masks {
142 | err = mask(pixbuf)
143 | if err != nil {
144 | s.errorHandler(fmt.Errorf("'%s' mask error: %v", id, err))
145 | return
146 | }
147 | }
148 |
149 | size := &Size{
150 | W: pixbuf.GetWidth(),
151 | H: pixbuf.GetHeight(),
152 | }
153 |
154 | s.SetFromPixbuf(pixbuf)
155 |
156 | if s.size == nil && s.readyHandler != nil {
157 | s.readyHandler(size)
158 | }
159 |
160 | if s.frameHandler != nil {
161 | s.frameHandler(size)
162 | }
163 |
164 | s.size = size
165 | })
166 | }
167 | }()
168 |
169 | s.Connect("destroy", func() {
170 | stream.Stop()
171 | app.Close()
172 | s = nil
173 | })
174 |
175 | return nil
176 | }
177 |
178 | func (s *Stream) CaptureFrame() error {
179 | app, err := wl.NewApp(hclog.Default())
180 | if err != nil {
181 | return fmt.Errorf("failed to wayland connection: %v", err)
182 | }
183 | defer app.Close()
184 |
185 | frame, err := app.CaptureFrame(s.handle)
186 | if err != nil {
187 | app.Close()
188 | return fmt.Errorf("failed to capture frame: %v", err)
189 | }
190 |
191 | pixbuf, err := nRGBAtoPixbuf(frame)
192 | if err != nil {
193 | return fmt.Errorf("failed to convert pixbuf: %v", err)
194 | }
195 |
196 | if s.scaleMode != nil {
197 | pixbuf, err = s.scale(pixbuf)
198 | if err != nil {
199 | return fmt.Errorf("failed to scale pixbuf: %v", err)
200 | }
201 | }
202 |
203 | for id, effect := range s.effects {
204 | err = effect(pixbuf)
205 | if err != nil {
206 | return fmt.Errorf("'%s' effect error: %v", id, err)
207 | }
208 | }
209 |
210 | for id, mask := range s.masks {
211 | err = mask(pixbuf)
212 | if err != nil {
213 | return fmt.Errorf("'%s' mask error: %v", id, err)
214 | }
215 | }
216 |
217 | size := &Size{
218 | W: pixbuf.GetWidth(),
219 | H: pixbuf.GetHeight(),
220 | }
221 |
222 | s.SetFromPixbuf(pixbuf)
223 |
224 | if s.readyHandler != nil {
225 | s.readyHandler(size)
226 | }
227 |
228 | s.size = size
229 |
230 | return nil
231 | }
232 |
233 | func (s *Stream) SetWScale(width int) {
234 | s.scaleMode = &ScaleMode{scaleW: width}
235 | }
236 |
237 | func (s *Stream) SetHScale(height int) {
238 | s.scaleMode = &ScaleMode{scaleH: height}
239 | }
240 |
241 | func (s *Stream) SetFScale(factor float64) {
242 | s.scaleMode = &ScaleMode{scaleF: factor}
243 | }
244 |
245 | func (s *Stream) SetFixedSize(width int, height int) {
246 | s.scaleMode = &ScaleMode{
247 | scaleW: width,
248 | scaleH: height,
249 | }
250 | }
251 |
252 | func (s *Stream) ResetSize() {
253 | s.scaleMode = nil
254 | }
255 |
256 | func (s *Stream) AddCustomEffect(id string, effectHandler func(p *gdk.Pixbuf) error) {
257 | if effectHandler == nil {
258 | return
259 | }
260 |
261 | s.effects[id] = effectHandler
262 | }
263 |
264 | func (s *Stream) RemoveCustomEffect(id string) bool {
265 | _, exist := s.effects[id]
266 | if exist {
267 | delete(s.effects, id)
268 | }
269 |
270 | return exist
271 | }
272 |
273 | func (s *Stream) AddCustomMask(id string, maskHandler func(p *gdk.Pixbuf) error) {
274 | if maskHandler == nil {
275 | return
276 | }
277 |
278 | s.masks[id] = maskHandler
279 | }
280 |
281 | func (s *Stream) RemoveCustomMask(id string) bool {
282 | _, exist := s.masks[id]
283 | if exist {
284 | delete(s.masks, id)
285 | }
286 |
287 | return exist
288 | }
289 |
290 | func (s *Stream) SetBorderRadius(radius int) {
291 | id := "system-border-radius:hysc00"
292 |
293 | if radius <= 0 {
294 | s.RemoveCustomMask(id)
295 | return
296 | }
297 |
298 | s.AddCustomMask(id, func(p *gdk.Pixbuf) error {
299 | w, h := p.GetWidth(), p.GetHeight()
300 | size := Size{radius, radius}
301 |
302 | ApplyVector(p, Cord{0, 0}, size, func(pixel Cord) float64 {
303 | return radiusmask(pixel, Cord{radius, radius}, float64(radius))
304 | })
305 | ApplyVector(p, Cord{w - radius, 0}, size, func(pixel Cord) float64 {
306 | return radiusmask(pixel, Cord{0, radius}, float64(radius))
307 | })
308 | ApplyVector(p, Cord{0, h - radius}, size, func(pixel Cord) float64 {
309 | return radiusmask(pixel, Cord{radius, 0}, float64(radius))
310 | })
311 | ApplyVector(p, Cord{w - radius, h - radius}, size, func(pixel Cord) float64 {
312 | return radiusmask(pixel, Cord{0, 0}, float64(radius))
313 | })
314 | return nil
315 | })
316 | }
317 |
318 | func (s *Stream) SetInterpType(interpType gdk.InterpType) {
319 | s.interpType = interpType
320 | }
321 |
322 | func (s *Stream) OnReady(handler func(*Size)) {
323 | s.readyHandler = handler
324 | }
325 |
326 | func (s *Stream) OnFrame(handler func(*Size)) {
327 | s.frameHandler = handler
328 | }
329 |
330 | func (s *Stream) OnError(handler func(error)) {
331 | s.errorHandler = handler
332 | }
333 |
334 | func (s *Stream) scale(pixbuf *gdk.Pixbuf) (*gdk.Pixbuf, error) {
335 | origWidth := pixbuf.GetWidth()
336 | origHeight := pixbuf.GetHeight()
337 |
338 | // fixed dimensions
339 | if s.scaleMode.scaleH > 0 && s.scaleMode.scaleW > 0 {
340 | return pixbuf.ScaleSimple(
341 | s.scaleMode.scaleW,
342 | s.scaleMode.scaleH,
343 | s.interpType,
344 | )
345 | }
346 |
347 | // fixed height (width proportional)
348 | if s.scaleMode.scaleH > 0 {
349 | newHeight := s.scaleMode.scaleH
350 | ratio := float64(origWidth) / float64(origHeight)
351 | newWidth := int(float64(newHeight) * ratio)
352 |
353 | return pixbuf.ScaleSimple(
354 | newWidth,
355 | newHeight,
356 | s.interpType,
357 | )
358 | }
359 |
360 | // fixed width (height proportional)
361 | if s.scaleMode.scaleW > 0 {
362 | newWidth := s.scaleMode.scaleW
363 | ratio := float64(origHeight) / float64(origWidth)
364 | newHeight := int(float64(newWidth) * ratio)
365 |
366 | return pixbuf.ScaleSimple(
367 | newWidth,
368 | newHeight,
369 | s.interpType,
370 | )
371 | }
372 |
373 | // scaling by factor
374 | if s.scaleMode.scaleF > 0 {
375 | factor := s.scaleMode.scaleF
376 | newWidth := int(float64(origWidth) * factor)
377 | newHeight := int(float64(origHeight) * factor)
378 |
379 | return pixbuf.ScaleSimple(
380 | newWidth,
381 | newHeight,
382 | s.interpType,
383 | )
384 | }
385 |
386 | // no scaling
387 | return pixbuf, nil
388 | }
389 |
--------------------------------------------------------------------------------
/pkg/wl/app.go:
--------------------------------------------------------------------------------
1 | package wl
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "image/color"
7 | "time"
8 |
9 | "github.com/hashicorp/go-hclog"
10 | "github.com/pdf/go-wayland/client"
11 | "golang.org/x/sys/unix"
12 | )
13 |
14 | type App struct {
15 | display *client.Display
16 | registry *client.Registry
17 | shm *client.Shm
18 | tl *HyprlandToplevelExportManagerV1
19 | log hclog.Logger
20 | }
21 |
22 | type shmPool struct {
23 | *client.ShmPool
24 | fd int
25 | data []byte
26 | }
27 |
28 | type FrameStream struct {
29 | Frames chan *image.NRGBA
30 | stop chan struct{}
31 | }
32 |
33 | func NewApp(log hclog.Logger) (*App, error) {
34 | display, err := client.Connect(``)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | registry, err := display.GetRegistry()
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | app := &App{
45 | display: display,
46 | registry: registry,
47 | log: log.Named(`wl`),
48 | }
49 | display.SetErrorHandler(app.handleDisplayError)
50 | registry.SetGlobalHandler(app.handleRegistryGlobal)
51 |
52 | // init registry
53 | if err := app.roundTrip(); err != nil {
54 | return nil, err
55 | }
56 |
57 | // get events
58 | if err := app.roundTrip(); err != nil {
59 | return nil, err
60 | }
61 |
62 | return app, nil
63 | }
64 |
65 | func (a *App) StartStream(handle uint64, fps int, bufferSize int) (*FrameStream, error) {
66 | stream := &FrameStream{
67 | Frames: make(chan *image.NRGBA, bufferSize),
68 | stop: make(chan struct{}),
69 | }
70 |
71 | go func() {
72 | ticker := time.NewTicker(time.Second / time.Duration(fps))
73 | defer ticker.Stop()
74 |
75 | for {
76 | select {
77 | case <-stream.stop:
78 | close(stream.Frames)
79 | return
80 | case <-ticker.C:
81 | frame, err := a.CaptureFrame(handle)
82 | if err != nil {
83 | a.log.Trace("Capture error: %v", err)
84 | continue
85 | }
86 |
87 | select {
88 | case stream.Frames <- frame:
89 | default:
90 | a.log.Trace("Buffer full")
91 | }
92 | }
93 | }
94 | }()
95 |
96 | return stream, nil
97 | }
98 |
99 | func (s *FrameStream) Stop() {
100 | close(s.stop)
101 | }
102 |
103 | func (a *App) CaptureFrame(handle uint64) (*image.NRGBA, error) {
104 | if a.tl == nil {
105 | return nil, fmt.Errorf(`toplevel export manager not available`)
106 | }
107 |
108 | frame, err := a.tl.CaptureToplevel(0, uint32(handle))
109 | if err != nil {
110 | return nil, err
111 | }
112 | defer func() {
113 | if err := frame.Destroy(); err != nil {
114 | a.log.Error(`failed destroying frame`, `err`, err)
115 | }
116 | }()
117 |
118 | formats := make([]HyprlandToplevelExportFrameV1BufferEvent, 0)
119 | done := make(chan struct{})
120 | ready := make(chan struct{})
121 | failed := make(chan error, 1)
122 | frame.SetBufferHandler(func(evt HyprlandToplevelExportFrameV1BufferEvent) {
123 | formats = append(formats, evt)
124 | })
125 | frame.SetBufferDoneHandler(func(evt HyprlandToplevelExportFrameV1BufferDoneEvent) {
126 | close(done)
127 | })
128 | frame.SetReadyHandler(func(evt HyprlandToplevelExportFrameV1ReadyEvent) {
129 | close(ready)
130 | })
131 | frame.SetFailedHandler(func(evt HyprlandToplevelExportFrameV1FailedEvent) {
132 | failed <- fmt.Errorf(`frame failed`)
133 | })
134 |
135 | if err := a.roundTrip(); err != nil {
136 | return nil, err
137 | }
138 |
139 | select {
140 | case <-done:
141 | case err := <-failed:
142 | return nil, err
143 | }
144 |
145 | if len(formats) == 0 {
146 | return nil, fmt.Errorf(`no buffer formats`)
147 | }
148 |
149 | a.log.Debug("Available buffer formats:", "count", len(formats))
150 | for i, format := range formats {
151 | a.log.Debug(fmt.Sprintf("Format %d:", i),
152 | "width", format.Width,
153 | "height", format.Height,
154 | "stride", format.Stride,
155 | "format", format.Format,
156 | "shm_format_name", client.ShmFormat(format.Format).String(),
157 | )
158 | }
159 |
160 | var selected *HyprlandToplevelExportFrameV1BufferEvent
161 | OUTER:
162 | for _, format := range formats {
163 | switch client.ShmFormat(format.Format) {
164 | case client.ShmFormatArgb8888:
165 | selected = &format
166 | break OUTER
167 | case client.ShmFormatXrgb8888:
168 | selected = &format
169 | break OUTER
170 | }
171 | }
172 |
173 | if selected == nil {
174 | return nil, fmt.Errorf(`no suitable buffer format`)
175 | }
176 |
177 | pool, err := a.createShmPool(int32(selected.Height * selected.Stride))
178 | if err != nil {
179 | return nil, err
180 | }
181 | defer func() {
182 | if err := pool.Close(); err != nil {
183 | a.log.Error(`failed closing SHM pool`, `err`, err)
184 | }
185 | }()
186 |
187 | buf, err := pool.CreateBuffer(0, int32(selected.Width), int32(selected.Height), int32(selected.Stride), selected.Format)
188 | if err != nil {
189 | return nil, err
190 | }
191 | defer func() {
192 | if err := buf.Destroy(); err != nil {
193 | a.log.Error(`failed destroying buffer`, `err`, err)
194 | }
195 | }()
196 |
197 | if err := frame.Copy(buf, 1); err != nil {
198 | return nil, err
199 | }
200 |
201 | if err := a.roundTrip(); err != nil {
202 | return nil, err
203 | }
204 |
205 | select {
206 | case <-ready:
207 | case err := <-failed:
208 | return nil, err
209 | }
210 |
211 | data := pool.Data()
212 | img := image.NewNRGBA(image.Rect(0, 0, int(selected.Width), int(selected.Height)))
213 | if len(img.Pix) < int(selected.Height)*int(selected.Stride) {
214 | return nil, fmt.Errorf(`image buffer too small`)
215 | }
216 | for y := range int(selected.Height) {
217 | for x := range int(selected.Width) {
218 | pix := data[y*int(selected.Stride)+(x*4) : y*int(selected.Stride)+(x*4)+4]
219 | col := color.NRGBA{}
220 | switch client.ShmFormat(selected.Format) {
221 | case client.ShmFormatArgb8888:
222 | col.A = pix[3]
223 | col.R = pix[2]
224 | col.G = pix[1]
225 | col.B = pix[0]
226 | case client.ShmFormatXrgb8888:
227 | col.A = 0xff
228 | col.R = pix[2]
229 | col.G = pix[1]
230 | col.B = pix[0]
231 | }
232 | img.SetNRGBA(x, y, col)
233 | }
234 | }
235 |
236 | return img, nil
237 | }
238 |
239 | func (a *App) Close() error {
240 | if a.tl != nil {
241 | if err := a.tl.Destroy(); err != nil {
242 | return err
243 | }
244 | }
245 | if a.shm != nil {
246 | if err := a.shm.Release(); err != nil {
247 | return nil
248 | }
249 | }
250 | if a.registry != nil {
251 | if err := a.registry.Destroy(); err != nil {
252 | return err
253 | }
254 | }
255 | if a.display != nil {
256 | if err := a.display.Destroy(); err != nil {
257 | return err
258 | }
259 | }
260 | return nil
261 | }
262 |
263 | func (p *shmPool) Data() []byte {
264 | return p.data
265 | }
266 |
267 | func (p *shmPool) Close() error {
268 | if err := unix.Munmap(p.data); err != nil {
269 | return err
270 | }
271 | if err := p.Destroy(); err != nil {
272 | return err
273 | }
274 | if err := unix.Close(p.fd); err != nil {
275 | return err
276 | }
277 | return nil
278 | }
279 |
280 | func (a *App) handleDisplayError(evt client.DisplayErrorEvent) {
281 | a.log.Trace("Display error occurred", "error", evt)
282 |
283 | err := a.reconnect()
284 | if err != nil {
285 | a.log.Trace("Reconnection failed", "error", err)
286 | }
287 |
288 | a.log.Trace("Successfully reconnected to Wayland display")
289 | }
290 |
291 | func (a *App) handleShmFormat(evt client.ShmFormatEvent) {
292 | a.log.Trace(`reported available SHM format`, `format`, client.ShmFormat(evt.Format))
293 | }
294 |
295 | func (a *App) handleRegistryGlobal(evt client.RegistryGlobalEvent) {
296 | a.log.Trace(`global object`, `name`, evt.Name, `interface`, evt.Interface, `version`, evt.Version)
297 |
298 | switch evt.Interface {
299 | case `wl_shm`:
300 | shm := client.NewShm(a.display.Context())
301 | if err := a.registry.Bind(evt.Name, evt.Interface, evt.Version, shm); err != nil {
302 | a.log.Error(`failed binding SHM`, `err`, err)
303 | return
304 | }
305 | shm.SetFormatHandler(a.handleShmFormat)
306 | a.shm = shm
307 | case `hyprland_toplevel_export_manager_v1`:
308 | tl := NewHyprlandToplevelExportManagerV1(a.display.Context())
309 | if err := a.registry.Bind(evt.Name, evt.Interface, evt.Version, tl); err != nil {
310 | a.log.Error(`failed binding toplevel export manager`, `err`, err)
311 | return
312 | }
313 | a.tl = tl
314 | }
315 | }
316 |
317 | func (a *App) reconnect() error {
318 | a.Close()
319 |
320 | display, err := client.Connect("")
321 | if err != nil {
322 | return fmt.Errorf("failed to reconnect: %w", err)
323 | }
324 |
325 | registry, err := display.GetRegistry()
326 | if err != nil {
327 | return fmt.Errorf("failed to get registry: %w", err)
328 | }
329 |
330 | a.display = display
331 | a.registry = registry
332 |
333 | display.SetErrorHandler(a.handleDisplayError)
334 | registry.SetGlobalHandler(a.handleRegistryGlobal)
335 |
336 | if err := a.roundTrip(); err != nil {
337 | return err
338 | }
339 |
340 | return a.roundTrip()
341 | }
342 |
343 | func (a *App) createShmPool(size int32) (*shmPool, error) {
344 | fd, err := unix.MemfdSecret(0)
345 | if err != nil {
346 | return nil, fmt.Errorf(`failed creating memfd: %w`, err)
347 | }
348 | if err := unix.Ftruncate(fd, int64(size)); err != nil {
349 | return nil, fmt.Errorf(`failed truncating memfd: %w`, err)
350 | }
351 |
352 | data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
353 | if err != nil {
354 | return nil, fmt.Errorf(`failed mmapping memfd: %w`, err)
355 | }
356 |
357 | pool, err := a.shm.CreatePool(fd, int32(size))
358 | if err != nil {
359 | return nil, fmt.Errorf(`failed creating SHM pool: %w`, err)
360 | }
361 |
362 | return &shmPool{
363 | ShmPool: pool,
364 | fd: fd,
365 | data: data,
366 | }, nil
367 | }
368 |
369 | func (a *App) roundTrip() error {
370 | cb, err := a.display.Sync()
371 | if err != nil {
372 | return err
373 | }
374 | defer func() {
375 | if err := cb.Destroy(); err != nil {
376 | a.log.Error(`failed destroying callback`, `err`, err)
377 | }
378 | }()
379 |
380 | done := make(chan struct{})
381 | cb.SetDoneHandler(func(_ client.CallbackDoneEvent) {
382 | close(done)
383 | })
384 |
385 | for {
386 | select {
387 | case <-done:
388 | return nil
389 | default:
390 | if err := a.display.Context().Dispatch(); err != nil {
391 | a.log.Trace(`dispatch error`, `err`, err)
392 | }
393 | }
394 | }
395 | }
396 |
--------------------------------------------------------------------------------
/pkg/wl/hyprland_toplevel_export.go:
--------------------------------------------------------------------------------
1 | // Generated by go-wayland-scanner
2 | // https://github.com/pdf/go-wayland/cmd/go-wayland-scanner
3 | // XML file : https://github.com/hyprwm/hyprland-protocols/raw/refs/heads/main/protocols/hyprland-toplevel-export-v1.xml
4 | //
5 | // hyprland_toplevel_export_v1 Protocol Copyright:
6 | //
7 | // Copyright © 2022 Vaxry
8 | // All rights reserved.
9 | //
10 | // Redistribution and use in source and binary forms, with or without
11 | // modification, are permitted provided that the following conditions are met:
12 | //
13 | // 1. Redistributions of source code must retain the above copyright notice, this
14 | // list of conditions and the following disclaimer.
15 | //
16 | // 2. Redistributions in binary form must reproduce the above copyright notice,
17 | // this list of conditions and the following disclaimer in the documentation
18 | // and/or other materials provided with the distribution.
19 | //
20 | // 3. Neither the name of the copyright holder nor the names of its
21 | // contributors may be used to endorse or promote products derived from
22 | // this software without specific prior written permission.
23 | //
24 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 |
35 | package wl
36 |
37 | import "github.com/pdf/go-wayland/client"
38 |
39 | // HyprlandToplevelExportManagerV1 : manager to inform clients and begin capturing
40 | //
41 | // This object is a manager which offers requests to start capturing from a
42 | // source.
43 | type HyprlandToplevelExportManagerV1 struct {
44 | client.BaseProxy
45 | }
46 |
47 | // NewHyprlandToplevelExportManagerV1 : manager to inform clients and begin capturing
48 | //
49 | // This object is a manager which offers requests to start capturing from a
50 | // source.
51 | func NewHyprlandToplevelExportManagerV1(ctx *client.Context) *HyprlandToplevelExportManagerV1 {
52 | hyprlandToplevelExportManagerV1 := &HyprlandToplevelExportManagerV1{}
53 | ctx.Register(hyprlandToplevelExportManagerV1)
54 | return hyprlandToplevelExportManagerV1
55 | }
56 |
57 | // CaptureToplevel : capture a toplevel
58 | //
59 | // Capture the next frame of a toplevel. (window)
60 | //
61 | // The captured frame will not contain any server-side decorations and will
62 | // ignore the compositor-set geometry, like e.g. rounded corners.
63 | //
64 | // It will contain all the subsurfaces and popups, however the latter will be clipped
65 | // to the geometry of the base surface.
66 | //
67 | // The handle parameter refers to the address of the window as seen in `hyprctl clients`.
68 | // For example, for d161e7b0 it would be 3512854448.
69 | //
70 | // overlayCursor: composite cursor onto the frame
71 | // handle: the handle of the toplevel (window) to be captured
72 | func (i *HyprlandToplevelExportManagerV1) CaptureToplevel(overlayCursor int32, handle uint32) (*HyprlandToplevelExportFrameV1, error) {
73 | frame := NewHyprlandToplevelExportFrameV1(i.Context())
74 | const opcode = 0
75 | const _reqBufLen = 8 + 4 + 4 + 4
76 | var _reqBuf [_reqBufLen]byte
77 | l := 0
78 | client.PutUint32(_reqBuf[l:4], i.ID())
79 | l += 4
80 | client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
81 | l += 4
82 | client.PutUint32(_reqBuf[l:l+4], frame.ID())
83 | l += 4
84 | client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
85 | l += 4
86 | client.PutUint32(_reqBuf[l:l+4], uint32(handle))
87 | err := i.Context().WriteMsg(_reqBuf[:], nil)
88 | return frame, err
89 | }
90 |
91 | // Destroy : destroy the manager
92 | //
93 | // All objects created by the manager will still remain valid, until their
94 | // appropriate destroy request has been called.
95 | func (i *HyprlandToplevelExportManagerV1) Destroy() error {
96 | defer i.Context().Unregister(i)
97 | const opcode = 1
98 | const _reqBufLen = 8
99 | var _reqBuf [_reqBufLen]byte
100 | l := 0
101 | client.PutUint32(_reqBuf[l:4], i.ID())
102 | l += 4
103 | client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
104 | err := i.Context().WriteMsg(_reqBuf[:], nil)
105 | return err
106 | }
107 |
108 | // CaptureToplevelWithWlrToplevelHandle : capture a toplevel
109 | //
110 | // Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle.
111 | //
112 | // overlayCursor: composite cursor onto the frame
113 | // handle: the zwlr_foreign_toplevel_handle_v1 handle of the toplevel to be captured
114 | func (i *HyprlandToplevelExportManagerV1) CaptureToplevelWithWlrToplevelHandle(overlayCursor int32, handle *ZwlrForeignToplevelHandleV1) (*HyprlandToplevelExportFrameV1, error) {
115 | frame := NewHyprlandToplevelExportFrameV1(i.Context())
116 | const opcode = 2
117 | const _reqBufLen = 8 + 4 + 4 + 4
118 | var _reqBuf [_reqBufLen]byte
119 | l := 0
120 | client.PutUint32(_reqBuf[l:4], i.ID())
121 | l += 4
122 | client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
123 | l += 4
124 | client.PutUint32(_reqBuf[l:l+4], frame.ID())
125 | l += 4
126 | client.PutUint32(_reqBuf[l:l+4], uint32(overlayCursor))
127 | l += 4
128 | client.PutUint32(_reqBuf[l:l+4], handle.ID())
129 | err := i.Context().WriteMsg(_reqBuf[:], nil)
130 | return frame, err
131 | }
132 |
133 | // HyprlandToplevelExportFrameV1 : a frame ready for copy
134 | //
135 | // This object represents a single frame.
136 | //
137 | // When created, a series of buffer events will be sent, each representing a
138 | // supported buffer type. The "buffer_done" event is sent afterwards to
139 | // indicate that all supported buffer types have been enumerated. The client
140 | // will then be able to send a "copy" request. If the capture is successful,
141 | // the compositor will send a "flags" followed by a "ready" event.
142 | //
143 | // wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent.
144 | //
145 | // If the capture failed, the "failed" event is sent. This can happen anytime
146 | // before the "ready" event.
147 | //
148 | // Once either a "ready" or a "failed" event is received, the client should
149 | // destroy the frame.
150 | type HyprlandToplevelExportFrameV1 struct {
151 | client.BaseProxy
152 | bufferHandler HyprlandToplevelExportFrameV1BufferHandlerFunc
153 | damageHandler HyprlandToplevelExportFrameV1DamageHandlerFunc
154 | flagsHandler HyprlandToplevelExportFrameV1FlagsHandlerFunc
155 | readyHandler HyprlandToplevelExportFrameV1ReadyHandlerFunc
156 | failedHandler HyprlandToplevelExportFrameV1FailedHandlerFunc
157 | linuxDmabufHandler HyprlandToplevelExportFrameV1LinuxDmabufHandlerFunc
158 | bufferDoneHandler HyprlandToplevelExportFrameV1BufferDoneHandlerFunc
159 | }
160 |
161 | // NewHyprlandToplevelExportFrameV1 : a frame ready for copy
162 | //
163 | // This object represents a single frame.
164 | //
165 | // When created, a series of buffer events will be sent, each representing a
166 | // supported buffer type. The "buffer_done" event is sent afterwards to
167 | // indicate that all supported buffer types have been enumerated. The client
168 | // will then be able to send a "copy" request. If the capture is successful,
169 | // the compositor will send a "flags" followed by a "ready" event.
170 | //
171 | // wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent.
172 | //
173 | // If the capture failed, the "failed" event is sent. This can happen anytime
174 | // before the "ready" event.
175 | //
176 | // Once either a "ready" or a "failed" event is received, the client should
177 | // destroy the frame.
178 | func NewHyprlandToplevelExportFrameV1(ctx *client.Context) *HyprlandToplevelExportFrameV1 {
179 | hyprlandToplevelExportFrameV1 := &HyprlandToplevelExportFrameV1{}
180 | ctx.Register(hyprlandToplevelExportFrameV1)
181 | return hyprlandToplevelExportFrameV1
182 | }
183 |
184 | // Copy : copy the frame
185 | //
186 | // Copy the frame to the supplied buffer. The buffer must have the
187 | // correct size, see hyprland_toplevel_export_frame_v1.buffer and
188 | // hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a
189 | // supported format.
190 | //
191 | // If the frame is successfully copied, a "flags" and a "ready" event is
192 | // sent. Otherwise, a "failed" event is sent.
193 | //
194 | // This event will wait for appropriate damage to be copied, unless the ignore_damage
195 | // arg is set to a non-zero value.
196 | func (i *HyprlandToplevelExportFrameV1) Copy(buffer *client.Buffer, ignoreDamage int32) error {
197 | const opcode = 0
198 | const _reqBufLen = 8 + 4 + 4
199 | var _reqBuf [_reqBufLen]byte
200 | l := 0
201 | client.PutUint32(_reqBuf[l:4], i.ID())
202 | l += 4
203 | client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
204 | l += 4
205 | client.PutUint32(_reqBuf[l:l+4], buffer.ID())
206 | l += 4
207 | client.PutUint32(_reqBuf[l:l+4], uint32(ignoreDamage))
208 | err := i.Context().WriteMsg(_reqBuf[:], nil)
209 | return err
210 | }
211 |
212 | // Destroy : delete this object, used or not
213 | //
214 | // Destroys the frame. This request can be sent at any time by the client.
215 | func (i *HyprlandToplevelExportFrameV1) Destroy() error {
216 | defer i.Context().Unregister(i)
217 | const opcode = 1
218 | const _reqBufLen = 8
219 | var _reqBuf [_reqBufLen]byte
220 | l := 0
221 | client.PutUint32(_reqBuf[l:4], i.ID())
222 | l += 4
223 | client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
224 | err := i.Context().WriteMsg(_reqBuf[:], nil)
225 | return err
226 | }
227 |
228 | type HyprlandToplevelExportFrameV1Error uint32
229 |
230 | // HyprlandToplevelExportFrameV1Error :
231 | const (
232 | // HyprlandToplevelExportFrameV1ErrorAlreadyUsed : the object has already been used to copy a wl_buffer
233 | HyprlandToplevelExportFrameV1ErrorAlreadyUsed HyprlandToplevelExportFrameV1Error = 0
234 | // HyprlandToplevelExportFrameV1ErrorInvalidBuffer : buffer attributes are invalid
235 | HyprlandToplevelExportFrameV1ErrorInvalidBuffer HyprlandToplevelExportFrameV1Error = 1
236 | )
237 |
238 | func (e HyprlandToplevelExportFrameV1Error) Name() string {
239 | switch e {
240 | case HyprlandToplevelExportFrameV1ErrorAlreadyUsed:
241 | return "already_used"
242 | case HyprlandToplevelExportFrameV1ErrorInvalidBuffer:
243 | return "invalid_buffer"
244 | default:
245 | return ""
246 | }
247 | }
248 |
249 | func (e HyprlandToplevelExportFrameV1Error) Value() string {
250 | switch e {
251 | case HyprlandToplevelExportFrameV1ErrorAlreadyUsed:
252 | return "0"
253 | case HyprlandToplevelExportFrameV1ErrorInvalidBuffer:
254 | return "1"
255 | default:
256 | return ""
257 | }
258 | }
259 |
260 | func (e HyprlandToplevelExportFrameV1Error) String() string {
261 | return e.Name() + "=" + e.Value()
262 | }
263 |
264 | type HyprlandToplevelExportFrameV1Flags uint32
265 |
266 | // HyprlandToplevelExportFrameV1Flags :
267 | const (
268 | // HyprlandToplevelExportFrameV1FlagsYInvert : contents are y-inverted
269 | HyprlandToplevelExportFrameV1FlagsYInvert HyprlandToplevelExportFrameV1Flags = 1
270 | )
271 |
272 | func (e HyprlandToplevelExportFrameV1Flags) Name() string {
273 | switch e {
274 | case HyprlandToplevelExportFrameV1FlagsYInvert:
275 | return "y_invert"
276 | default:
277 | return ""
278 | }
279 | }
280 |
281 | func (e HyprlandToplevelExportFrameV1Flags) Value() string {
282 | switch e {
283 | case HyprlandToplevelExportFrameV1FlagsYInvert:
284 | return "1"
285 | default:
286 | return ""
287 | }
288 | }
289 |
290 | func (e HyprlandToplevelExportFrameV1Flags) String() string {
291 | return e.Name() + "=" + e.Value()
292 | }
293 |
294 | // HyprlandToplevelExportFrameV1BufferEvent : wl_shm buffer information
295 | //
296 | // Provides information about wl_shm buffer parameters that need to be
297 | // used for this frame. This event is sent once after the frame is created
298 | // if wl_shm buffers are supported.
299 | type HyprlandToplevelExportFrameV1BufferEvent struct {
300 | Format uint32
301 | Width uint32
302 | Height uint32
303 | Stride uint32
304 | }
305 | type HyprlandToplevelExportFrameV1BufferHandlerFunc func(HyprlandToplevelExportFrameV1BufferEvent)
306 |
307 | // SetBufferHandler : sets handler for HyprlandToplevelExportFrameV1BufferEvent
308 | func (i *HyprlandToplevelExportFrameV1) SetBufferHandler(f HyprlandToplevelExportFrameV1BufferHandlerFunc) {
309 | i.bufferHandler = f
310 | }
311 |
312 | // HyprlandToplevelExportFrameV1DamageEvent : carries the coordinates of the damaged region
313 | //
314 | // This event is sent right before the ready event when ignore_damage was
315 | // not set. It may be generated multiple times for each copy
316 | // request.
317 | //
318 | // The arguments describe a box around an area that has changed since the
319 | // last copy request that was derived from the current screencopy manager
320 | // instance.
321 | //
322 | // The union of all regions received between the call to copy
323 | // and a ready event is the total damage since the prior ready event.
324 | type HyprlandToplevelExportFrameV1DamageEvent struct {
325 | X uint32
326 | Y uint32
327 | Width uint32
328 | Height uint32
329 | }
330 | type HyprlandToplevelExportFrameV1DamageHandlerFunc func(HyprlandToplevelExportFrameV1DamageEvent)
331 |
332 | // SetDamageHandler : sets handler for HyprlandToplevelExportFrameV1DamageEvent
333 | func (i *HyprlandToplevelExportFrameV1) SetDamageHandler(f HyprlandToplevelExportFrameV1DamageHandlerFunc) {
334 | i.damageHandler = f
335 | }
336 |
337 | // HyprlandToplevelExportFrameV1FlagsEvent : frame flags
338 | //
339 | // Provides flags about the frame. This event is sent once before the
340 | // "ready" event.
341 | type HyprlandToplevelExportFrameV1FlagsEvent struct {
342 | Flags uint32
343 | }
344 | type HyprlandToplevelExportFrameV1FlagsHandlerFunc func(HyprlandToplevelExportFrameV1FlagsEvent)
345 |
346 | // SetFlagsHandler : sets handler for HyprlandToplevelExportFrameV1FlagsEvent
347 | func (i *HyprlandToplevelExportFrameV1) SetFlagsHandler(f HyprlandToplevelExportFrameV1FlagsHandlerFunc) {
348 | i.flagsHandler = f
349 | }
350 |
351 | // HyprlandToplevelExportFrameV1ReadyEvent : indicates frame is available for reading
352 | //
353 | // Called as soon as the frame is copied, indicating it is available
354 | // for reading. This event includes the time at which presentation happened
355 | // at.
356 | //
357 | // The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples,
358 | // each component being an unsigned 32-bit value. Whole seconds are in
359 | // tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo,
360 | // and the additional fractional part in tv_nsec as nanoseconds. Hence,
361 | // for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part
362 | // may have an arbitrary offset at start.
363 | //
364 | // After receiving this event, the client should destroy the object.
365 | type HyprlandToplevelExportFrameV1ReadyEvent struct {
366 | TvSecHi uint32
367 | TvSecLo uint32
368 | TvNsec uint32
369 | }
370 | type HyprlandToplevelExportFrameV1ReadyHandlerFunc func(HyprlandToplevelExportFrameV1ReadyEvent)
371 |
372 | // SetReadyHandler : sets handler for HyprlandToplevelExportFrameV1ReadyEvent
373 | func (i *HyprlandToplevelExportFrameV1) SetReadyHandler(f HyprlandToplevelExportFrameV1ReadyHandlerFunc) {
374 | i.readyHandler = f
375 | }
376 |
377 | // HyprlandToplevelExportFrameV1FailedEvent : frame copy failed
378 | //
379 | // This event indicates that the attempted frame copy has failed.
380 | //
381 | // After receiving this event, the client should destroy the object.
382 | type HyprlandToplevelExportFrameV1FailedEvent struct{}
383 | type HyprlandToplevelExportFrameV1FailedHandlerFunc func(HyprlandToplevelExportFrameV1FailedEvent)
384 |
385 | // SetFailedHandler : sets handler for HyprlandToplevelExportFrameV1FailedEvent
386 | func (i *HyprlandToplevelExportFrameV1) SetFailedHandler(f HyprlandToplevelExportFrameV1FailedHandlerFunc) {
387 | i.failedHandler = f
388 | }
389 |
390 | // HyprlandToplevelExportFrameV1LinuxDmabufEvent : linux-dmabuf buffer information
391 | //
392 | // Provides information about linux-dmabuf buffer parameters that need to
393 | // be used for this frame. This event is sent once after the frame is
394 | // created if linux-dmabuf buffers are supported.
395 | type HyprlandToplevelExportFrameV1LinuxDmabufEvent struct {
396 | Format uint32
397 | Width uint32
398 | Height uint32
399 | }
400 | type HyprlandToplevelExportFrameV1LinuxDmabufHandlerFunc func(HyprlandToplevelExportFrameV1LinuxDmabufEvent)
401 |
402 | // SetLinuxDmabufHandler : sets handler for HyprlandToplevelExportFrameV1LinuxDmabufEvent
403 | func (i *HyprlandToplevelExportFrameV1) SetLinuxDmabufHandler(f HyprlandToplevelExportFrameV1LinuxDmabufHandlerFunc) {
404 | i.linuxDmabufHandler = f
405 | }
406 |
407 | // HyprlandToplevelExportFrameV1BufferDoneEvent : all buffer types reported
408 | //
409 | // This event is sent once after all buffer events have been sent.
410 | //
411 | // The client should proceed to create a buffer of one of the supported
412 | // types, and send a "copy" request.
413 | type HyprlandToplevelExportFrameV1BufferDoneEvent struct{}
414 | type HyprlandToplevelExportFrameV1BufferDoneHandlerFunc func(HyprlandToplevelExportFrameV1BufferDoneEvent)
415 |
416 | // SetBufferDoneHandler : sets handler for HyprlandToplevelExportFrameV1BufferDoneEvent
417 | func (i *HyprlandToplevelExportFrameV1) SetBufferDoneHandler(f HyprlandToplevelExportFrameV1BufferDoneHandlerFunc) {
418 | i.bufferDoneHandler = f
419 | }
420 |
421 | func (i *HyprlandToplevelExportFrameV1) Dispatch(opcode uint32, fd int, data []byte) {
422 | switch opcode {
423 | case 0:
424 | if i.bufferHandler == nil {
425 | return
426 | }
427 | var e HyprlandToplevelExportFrameV1BufferEvent
428 | l := 0
429 | e.Format = client.Uint32(data[l : l+4])
430 | l += 4
431 | e.Width = client.Uint32(data[l : l+4])
432 | l += 4
433 | e.Height = client.Uint32(data[l : l+4])
434 | l += 4
435 | e.Stride = client.Uint32(data[l : l+4])
436 |
437 | i.bufferHandler(e)
438 | case 1:
439 | if i.damageHandler == nil {
440 | return
441 | }
442 | var e HyprlandToplevelExportFrameV1DamageEvent
443 | l := 0
444 | e.X = client.Uint32(data[l : l+4])
445 | l += 4
446 | e.Y = client.Uint32(data[l : l+4])
447 | l += 4
448 | e.Width = client.Uint32(data[l : l+4])
449 | l += 4
450 | e.Height = client.Uint32(data[l : l+4])
451 |
452 | i.damageHandler(e)
453 | case 2:
454 | if i.flagsHandler == nil {
455 | return
456 | }
457 | var e HyprlandToplevelExportFrameV1FlagsEvent
458 | l := 0
459 | e.Flags = client.Uint32(data[l : l+4])
460 |
461 | i.flagsHandler(e)
462 | case 3:
463 | if i.readyHandler == nil {
464 | return
465 | }
466 | var e HyprlandToplevelExportFrameV1ReadyEvent
467 | l := 0
468 | e.TvSecHi = client.Uint32(data[l : l+4])
469 | l += 4
470 | e.TvSecLo = client.Uint32(data[l : l+4])
471 | l += 4
472 | e.TvNsec = client.Uint32(data[l : l+4])
473 |
474 | i.readyHandler(e)
475 | case 4:
476 | if i.failedHandler == nil {
477 | return
478 | }
479 | var e HyprlandToplevelExportFrameV1FailedEvent
480 |
481 | i.failedHandler(e)
482 | case 5:
483 | if i.linuxDmabufHandler == nil {
484 | return
485 | }
486 | var e HyprlandToplevelExportFrameV1LinuxDmabufEvent
487 | l := 0
488 | e.Format = client.Uint32(data[l : l+4])
489 | l += 4
490 | e.Width = client.Uint32(data[l : l+4])
491 | l += 4
492 | e.Height = client.Uint32(data[l : l+4])
493 |
494 | i.linuxDmabufHandler(e)
495 | case 6:
496 | if i.bufferDoneHandler == nil {
497 | return
498 | }
499 | var e HyprlandToplevelExportFrameV1BufferDoneEvent
500 |
501 | i.bufferDoneHandler(e)
502 | }
503 | }
504 |
--------------------------------------------------------------------------------