initial commit
This commit is contained in:
commit
58313e5490
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*_test.go
|
41
check_reachable.go
Normal file
41
check_reachable.go
Normal file
@ -0,0 +1,41 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IsReachable pings the unix socket to check
|
||||
// if Hyprland is ready to accept incoming requests
|
||||
func (c *Client) IsReachable() bool {
|
||||
str, err := readSocketString(c.SocketPath(), strings.NewReader("dispatch exec echo"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return str == "ok"
|
||||
}
|
||||
|
||||
// WaitUntilReachable periodically calls IsReachable
|
||||
// and returns true as soon as IsReachable returns true.
|
||||
// When ctx is closed before Hyprland is reachable, false is returned
|
||||
func (c *Client) WaitUntilReachable(ctx context.Context) bool {
|
||||
if c.IsReachable() {
|
||||
return true
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if c.IsReachable() {
|
||||
return true
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
24
dispatch.go
Normal file
24
dispatch.go
Normal file
@ -0,0 +1,24 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *Client) Dispatch(cmd string) (io.ReadCloser, error) {
|
||||
return readSocketRaw(c.SocketPath(), strings.NewReader(fmt.Sprintf("dispatch %s", cmd)))
|
||||
}
|
||||
|
||||
func (c *Client) DispatchExpectOK(cmd string) error {
|
||||
str, err := readSocketString(c.SocketPath(), strings.NewReader(fmt.Sprintf("dispatch %s", cmd)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(strings.TrimSpace(str)) != "ok" {
|
||||
return fmt.Errorf("dispatcher '%s' returned an error: %s", cmd, str)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
104
get_clients.go
Normal file
104
get_clients.go
Normal file
@ -0,0 +1,104 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.tordarus.net/tordarus/slices"
|
||||
)
|
||||
|
||||
func GetClient(signature string) (*Client, error) {
|
||||
lockFilePath := fmt.Sprintf("/run/user/1000/hypr/%s/hyprland.lock", signature)
|
||||
|
||||
file, err := os.Open(lockFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
pid, err := strconv.Atoi(lines[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
Signature: signature,
|
||||
PID: pid,
|
||||
WaylandSocket: lines[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetDefaultClient() (*Client, error) {
|
||||
signature, ok := os.LookupEnv("HYPRLAND_INSTANCE_SIGNATURE")
|
||||
if !ok {
|
||||
return nil, errors.New("default instance not found because HYPRLAND_INSTANCE_SIGNATURE is not set")
|
||||
}
|
||||
|
||||
return GetClient(signature)
|
||||
}
|
||||
|
||||
func GetClients() ([]*Client, error) {
|
||||
entries, err := os.ReadDir("/run/user/1000/hypr")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries = slices.Filter(entries, fs.DirEntry.IsDir)
|
||||
clients := make([]*Client, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
client, err := GetClient(entry.Name())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
clients = append(clients, client)
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func WaitForDefaultClient(ctx context.Context) (*Client, error) {
|
||||
signature, ok := os.LookupEnv("HYPRLAND_INSTANCE_SIGNATURE")
|
||||
if !ok {
|
||||
return nil, errors.New("default instance not found because HYPRLAND_INSTANCE_SIGNATURE is not set")
|
||||
}
|
||||
|
||||
return WaitForClient(ctx, signature)
|
||||
}
|
||||
|
||||
func WaitForClient(ctx context.Context, signature string) (*Client, error) {
|
||||
lockFilePath := fmt.Sprintf("/run/user/1000/hypr/%s/hyprland.lock", signature)
|
||||
|
||||
lockFileExists := waitFor(ctx, func() bool {
|
||||
_, err := os.Stat(lockFilePath)
|
||||
return !errors.Is(err, os.ErrNotExist)
|
||||
})
|
||||
|
||||
if !lockFileExists {
|
||||
return nil, errors.New("hyprland lock file not found")
|
||||
}
|
||||
|
||||
client, err := GetClient(signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !client.WaitUntilReachable(context.Background()) {
|
||||
return nil, errors.New("hyprland not reachable")
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
33
getters.go
Normal file
33
getters.go
Normal file
@ -0,0 +1,33 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (i *Client) GetActiveWindow() (*Window, error) {
|
||||
return readSocket[*Window](i.SocketPath(), strings.NewReader("j/activewindow"))
|
||||
}
|
||||
|
||||
func (i *Client) GetActiveWorkspace() (*Workspace, error) {
|
||||
return readSocket[*Workspace](i.SocketPath(), strings.NewReader("j/activeworkspace"))
|
||||
}
|
||||
|
||||
func (i *Client) GetBinds() ([]*Bind, error) {
|
||||
return readSocket[[]*Bind](i.SocketPath(), strings.NewReader("j/binds"))
|
||||
}
|
||||
|
||||
func (i *Client) GetWindows() ([]*Window, error) {
|
||||
return readSocket[[]*Window](i.SocketPath(), strings.NewReader("j/clients"))
|
||||
}
|
||||
|
||||
func (i *Client) GetCursorPos() (Point, error) {
|
||||
return readSocket[Point](i.SocketPath(), strings.NewReader("j/cursorpos"))
|
||||
}
|
||||
|
||||
func (i *Client) GetMonitors() ([]*Monitor, error) {
|
||||
return readSocket[[]*Monitor](i.SocketPath(), strings.NewReader("j/monitors"))
|
||||
}
|
||||
|
||||
func (i *Client) GetWorkspaces() ([]*Workspace, error) {
|
||||
return readSocket[[]*Workspace](i.SocketPath(), strings.NewReader("j/workspaces"))
|
||||
}
|
7
go.mod
Normal file
7
go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module git.tordarus.net/hyprland-tools/hypr
|
||||
|
||||
go 1.21.6
|
||||
|
||||
require git.tordarus.net/tordarus/slices v0.0.14
|
||||
|
||||
require git.tordarus.net/tordarus/gmath v0.0.7 // indirect
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
git.tordarus.net/tordarus/gmath v0.0.7 h1:tR48idt9AUL0r556ww3ZxByTKJEr6NWCTlhl2ihzYxQ=
|
||||
git.tordarus.net/tordarus/gmath v0.0.7/go.mod h1:mO7aPlvNrGVE9UFXEuuACjZgMDsM63l3OcQy6xSQnoE=
|
||||
git.tordarus.net/tordarus/slices v0.0.14 h1:Jy1VRMs777WewJ7mxTgjyQIMm/Zr+co18/XoQ01YZ3A=
|
||||
git.tordarus.net/tordarus/slices v0.0.14/go.mod h1:RgE7A1aSAezIvPUgcbUuMHu0q4xGKoRevT+DC0eJmwI=
|
22
model_bind.go
Normal file
22
model_bind.go
Normal file
@ -0,0 +1,22 @@
|
||||
package hypr
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Bind struct {
|
||||
Locked bool `json:"locked"`
|
||||
Mouse bool `json:"mouse"`
|
||||
Release bool `json:"release"`
|
||||
Repeat bool `json:"repeat"`
|
||||
NonConsuming bool `json:"non_consuming"`
|
||||
ModMask int `json:"modmask"`
|
||||
Submap string `json:"submap"`
|
||||
Key string `json:"key"`
|
||||
KeyCode int `json:"keycode"`
|
||||
Dispatcher string `json:"dispatcher"`
|
||||
Arg string `json:"arg"`
|
||||
}
|
||||
|
||||
func (b Bind) String() string {
|
||||
data, _ := json.MarshalIndent(b, "", "\t")
|
||||
return string(data)
|
||||
}
|
70
model_event.go
Normal file
70
model_event.go
Normal file
@ -0,0 +1,70 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type EventType `json:"type"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
func parseEvent(str string) Event {
|
||||
data := strings.Split(str, ">>")
|
||||
return Event{
|
||||
Type: EventType(data[0]),
|
||||
Data: strings.Split(data[1], ","),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Event) String() string {
|
||||
data, _ := json.MarshalIndent(e, "", "\t")
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type EventType = string
|
||||
|
||||
const (
|
||||
EventTypeTick EventType = "tick"
|
||||
EventTypeActiveWindow EventType = "activewindow"
|
||||
EventTypeActiveWindowV2 EventType = "activewindowv2"
|
||||
EventTypeKeyboardFocus EventType = "keyboardfocus"
|
||||
EventTypeMoveWorkspace EventType = "moveworkspace"
|
||||
EventTypeMoveWorkspaceV2 EventType = "moveworkspacev2"
|
||||
EventTypeFocusedMon EventType = "focusedmon"
|
||||
EventTypeMoveWindow EventType = "movewindow"
|
||||
EventTypeMoveWindowV2 EventType = "movewindowv2"
|
||||
EventTypeOpenLayer EventType = "openlayer"
|
||||
EventTypeCloseLayer EventType = "closelayer"
|
||||
EventTypeOpenWindow EventType = "openwindow"
|
||||
EventTypeCloseWindow EventType = "closewindow"
|
||||
EventTypeUrgent EventType = "urgent"
|
||||
EventTypeMinimize EventType = "minimize"
|
||||
EventTypeMonitorAdded EventType = "monitoradded"
|
||||
EventTypeMonitorAddedV2 EventType = "monitoraddedv2"
|
||||
EventTypeMonitorRemoved EventType = "monitorremoved"
|
||||
EventTypeCreateWorkspace EventType = "createworkspace"
|
||||
EventTypeCreateWorkspaceV2 EventType = "createworkspacev2"
|
||||
EventTypeDestroyWorkspace EventType = "destroyworkspace"
|
||||
EventTypeDestroyWorkspaceV2 EventType = "destroyworkspacev2"
|
||||
EventTypeFullscreen EventType = "fullscreen"
|
||||
EventTypeChangeFloatingMode EventType = "changefloatingmode"
|
||||
EventTypeWorkspace EventType = "workspace"
|
||||
EventTypeWorkspaceV2 EventType = "workspacev2"
|
||||
EventTypeSubmap EventType = "submap"
|
||||
EventTypeMouseMove EventType = "mousemove"
|
||||
EventTypeMouseButton EventType = "mousebutton"
|
||||
EventTypeMouseAxis EventType = "mouseaxis"
|
||||
EventTypeTouchDown EventType = "touchdown"
|
||||
EventTypeTouchUp EventType = "touchup"
|
||||
EventTypeTouchMove EventType = "touchmove"
|
||||
EventTypeActiveLayout EventType = "activelayout"
|
||||
EventTypePreRender EventType = "prerender"
|
||||
EventTypeScreencast EventType = "screencast"
|
||||
EventTypeRender EventType = "render"
|
||||
EventTypeWindowtitle EventType = "windowtitle"
|
||||
EventTypeConfigReloaded EventType = "configreloaded"
|
||||
EventTypePreConfigReload EventType = "preconfigreload"
|
||||
EventTypeKeyPress EventType = "keypress"
|
||||
)
|
25
model_instance.go
Normal file
25
model_instance.go
Normal file
@ -0,0 +1,25 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Signature string `json:"instance"`
|
||||
PID int `json:"pid"`
|
||||
WaylandSocket string `json:"wl_socket"`
|
||||
}
|
||||
|
||||
func (c Client) String() string {
|
||||
data, _ := json.MarshalIndent(c, "", "\t")
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (c Client) SocketPath() string {
|
||||
return fmt.Sprintf("/run/user/1000/hypr/%s/.socket.sock", c.Signature)
|
||||
}
|
||||
|
||||
func (c Client) EventSocketPath() string {
|
||||
return fmt.Sprintf("/run/user/1000/hypr/%s/.socket2.sock", c.Signature)
|
||||
}
|
13
model_misc.go
Normal file
13
model_misc.go
Normal file
@ -0,0 +1,13 @@
|
||||
package hypr
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Point struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
func (p Point) String() string {
|
||||
data, _ := json.MarshalIndent(p, "", "\t")
|
||||
return string(data)
|
||||
}
|
31
model_monitor.go
Normal file
31
model_monitor.go
Normal file
@ -0,0 +1,31 @@
|
||||
package hypr
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Monitor struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Make string `json:"make"`
|
||||
Model string `json:"model"`
|
||||
Serial string `json:"serial"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
RefreshRate float64 `json:"refreshRate"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
ActiveWorkspace WorkspaceIdent `json:"activeWorkspace"`
|
||||
SpecialWorkspace WorkspaceIdent `json:"specialWorkspace"`
|
||||
Reserved [4]int `json:"reserved"`
|
||||
Scale float64 `json:"scale"`
|
||||
Transform int `json:"transform"`
|
||||
Focused bool `json:"focused"`
|
||||
DPMSStatus bool `json:"dpmsStatus"`
|
||||
VRR bool `json:"vrr"`
|
||||
ActivelyTearing bool `json:"activelyTearing"`
|
||||
}
|
||||
|
||||
func (m Monitor) String() string {
|
||||
data, _ := json.MarshalIndent(m, "", "\t")
|
||||
return string(data)
|
||||
}
|
41
model_window.go
Normal file
41
model_window.go
Normal file
@ -0,0 +1,41 @@
|
||||
package hypr
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Window struct {
|
||||
Address string `json:"address"`
|
||||
Mapped bool `json:"mapped"`
|
||||
Hidden bool `json:"hidden"`
|
||||
At [2]int `json:"at"`
|
||||
Size [2]int `json:"size"`
|
||||
Workspace WorkspaceIdent `json:"workspace"`
|
||||
Floating bool `json:"floating"`
|
||||
MonitorID int `json:"monitor"`
|
||||
Class string `json:"class"`
|
||||
Title string `json:"title"`
|
||||
InitialClass string `json:"initialClass"`
|
||||
InitialTitle string `json:"initialTitle"`
|
||||
PID int `json:"pid"`
|
||||
Xwayland bool `json:"xwayland"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Fullscreen FullscreenState `json:"fullscreen"`
|
||||
FullscreenClient FullscreenState `json:"fullscreenClient"`
|
||||
Grouped []interface{} `json:"grouped"` // TODO
|
||||
Swallowing string `json:"swallowing"`
|
||||
FocusHistoryID int `json:"focusHistoryID"`
|
||||
}
|
||||
|
||||
func (w Window) String() string {
|
||||
data, _ := json.MarshalIndent(w, "", "\t")
|
||||
return string(data)
|
||||
}
|
||||
|
||||
type FullscreenState int
|
||||
|
||||
const (
|
||||
FullscreenCurrent FullscreenState = -1
|
||||
FullscreenNone FullscreenState = 0
|
||||
FullscreenMaximize FullscreenState = 1
|
||||
FullscreenFullscreen FullscreenState = 2
|
||||
FullscreenMaximizeFullscreen FullscreenState = 3
|
||||
)
|
24
model_workspace.go
Normal file
24
model_workspace.go
Normal file
@ -0,0 +1,24 @@
|
||||
package hypr
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type WorkspaceIdent struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Monitor string `json:"monitor"`
|
||||
MonitorID int `json:"monitorID"`
|
||||
Windows int `json:"windows"`
|
||||
HasFullscreen bool `json:"hasfullscreen"`
|
||||
LastWindow string `json:"lastwindow"`
|
||||
LastWindowTitle string `json:"lastwindowtitle"`
|
||||
}
|
||||
|
||||
func (w Workspace) String() string {
|
||||
data, _ := json.MarshalIndent(w, "", "\t")
|
||||
return string(data)
|
||||
}
|
36
subscribe_event.go
Normal file
36
subscribe_event.go
Normal file
@ -0,0 +1,36 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.tordarus.net/tordarus/slices"
|
||||
)
|
||||
|
||||
func (i *Client) Subscribe(ctx context.Context, events ...EventType) (<-chan Event, error) {
|
||||
r, err := readSocketRaw(i.EventSocketPath(), strings.NewReader(""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(chan Event, 10)
|
||||
eventMap := slices.ToStructMap(events)
|
||||
allEvents := len(events) == 0
|
||||
|
||||
go func() {
|
||||
defer r.Close()
|
||||
defer close(out)
|
||||
|
||||
sc := bufio.NewScanner(r)
|
||||
|
||||
for ctx.Err() == nil && sc.Scan() {
|
||||
event := parseEvent(sc.Text())
|
||||
if _, ok := eventMap[event.Type]; allEvents || ok {
|
||||
out <- event
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return out, nil
|
||||
}
|
73
utils.go
Normal file
73
utils.go
Normal file
@ -0,0 +1,73 @@
|
||||
package hypr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func readSocketRaw(socket string, body io.Reader) (io.ReadCloser, error) {
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(conn, body); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func readSocketString(socket string, body io.Reader) (string, error) {
|
||||
r, err := readSocketRaw(socket, body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func readSocket[T any](socket string, body io.Reader) (T, error) {
|
||||
r, err := readSocketRaw(socket, body)
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
value := new(T)
|
||||
if err := json.NewDecoder(r).Decode(value); err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
|
||||
return *value, nil
|
||||
}
|
||||
|
||||
func waitFor(ctx context.Context, condition func() bool) bool {
|
||||
if condition() {
|
||||
return true
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if condition() {
|
||||
return true
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user