Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,12 +6,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) Dispatch(cmd string) (io.ReadCloser, error) {
|
func (i *Instance) Dispatch(cmd string) (io.ReadCloser, error) {
|
||||||
return readSocketRaw(c.SocketPath(), strings.NewReader(fmt.Sprintf("dispatch %s", cmd)))
|
return readSocketRaw(i.SocketPath(), strings.NewReader(cmd))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DispatchExpectOK(cmd string) error {
|
func (i *Instance) DispatchExpectOK(cmd string) error {
|
||||||
str, err := readSocketString(c.SocketPath(), strings.NewReader(fmt.Sprintf("dispatch %s", cmd)))
|
str, err := readSocketString(i.SocketPath(), strings.NewReader(cmd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
104
get_clients.go
104
get_clients.go
@ -1,104 +0,0 @@
|
|||||||
package hypr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.milar.in/milarin/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
|
|
||||||
}
|
|
70
get_instances.go
Normal file
70
get_instances.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package hypr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.milar.in/milarin/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetInstance(signature string) (*Instance, error) {
|
||||||
|
lockFilePath := fmt.Sprintf("/tmp/hypr/%s.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 &Instance{
|
||||||
|
Signature: signature,
|
||||||
|
PID: pid,
|
||||||
|
WaylandSocket: lines[1],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultInstance() (*Instance, 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 GetInstance(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInstances() ([]*Instance, error) {
|
||||||
|
entries, err := os.ReadDir("/tmp/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = slices.Filter(entries, fs.DirEntry.IsDir)
|
||||||
|
instances := make([]*Instance, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
instance, err := GetInstance(entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instances = append(instances, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances, nil
|
||||||
|
}
|
14
getters.go
14
getters.go
@ -4,30 +4,30 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i *Client) GetActiveWindow() (*Window, error) {
|
func (i *Instance) GetActiveWindow() (*Window, error) {
|
||||||
return readSocket[*Window](i.SocketPath(), strings.NewReader("j/activewindow"))
|
return readSocket[*Window](i.SocketPath(), strings.NewReader("j/activewindow"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Client) GetActiveWorkspace() (*Workspace, error) {
|
func (i *Instance) GetActiveWorkspace() (*Workspace, error) {
|
||||||
return readSocket[*Workspace](i.SocketPath(), strings.NewReader("j/activeworkspace"))
|
return readSocket[*Workspace](i.SocketPath(), strings.NewReader("j/activeworkspace"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Client) GetBinds() ([]*Bind, error) {
|
func (i *Instance) GetBinds() ([]*Bind, error) {
|
||||||
return readSocket[[]*Bind](i.SocketPath(), strings.NewReader("j/binds"))
|
return readSocket[[]*Bind](i.SocketPath(), strings.NewReader("j/binds"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Client) GetWindows() ([]*Window, error) {
|
func (i *Instance) GetWindows() ([]*Window, error) {
|
||||||
return readSocket[[]*Window](i.SocketPath(), strings.NewReader("j/clients"))
|
return readSocket[[]*Window](i.SocketPath(), strings.NewReader("j/clients"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Client) GetCursorPos() (Point, error) {
|
func (i *Instance) GetCursorPos() (Point, error) {
|
||||||
return readSocket[Point](i.SocketPath(), strings.NewReader("j/cursorpos"))
|
return readSocket[Point](i.SocketPath(), strings.NewReader("j/cursorpos"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Client) GetMonitors() ([]*Monitor, error) {
|
func (i *Instance) GetMonitors() ([]*Monitor, error) {
|
||||||
return readSocket[[]*Monitor](i.SocketPath(), strings.NewReader("j/monitors"))
|
return readSocket[[]*Monitor](i.SocketPath(), strings.NewReader("j/monitors"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Client) GetWorkspaces() ([]*Workspace, error) {
|
func (i *Instance) GetWorkspaces() ([]*Workspace, error) {
|
||||||
return readSocket[[]*Workspace](i.SocketPath(), strings.NewReader("j/workspaces"))
|
return readSocket[[]*Workspace](i.SocketPath(), strings.NewReader("j/workspaces"))
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -1,4 +1,4 @@
|
|||||||
module git.milar.in/hyprland-tools/hypr
|
module git.milar.in/milarin/hypr
|
||||||
|
|
||||||
go 1.21.6
|
go 1.21.6
|
||||||
|
|
||||||
|
@ -31,10 +31,8 @@ const (
|
|||||||
EventTypeActiveWindowV2 EventType = "activewindowv2"
|
EventTypeActiveWindowV2 EventType = "activewindowv2"
|
||||||
EventTypeKeyboardFocus EventType = "keyboardfocus"
|
EventTypeKeyboardFocus EventType = "keyboardfocus"
|
||||||
EventTypeMoveWorkspace EventType = "moveworkspace"
|
EventTypeMoveWorkspace EventType = "moveworkspace"
|
||||||
EventTypeMoveWorkspaceV2 EventType = "moveworkspacev2"
|
|
||||||
EventTypeFocusedMon EventType = "focusedmon"
|
EventTypeFocusedMon EventType = "focusedmon"
|
||||||
EventTypeMoveWindow EventType = "movewindow"
|
EventTypeMoveWindow EventType = "movewindow"
|
||||||
EventTypeMoveWindowV2 EventType = "movewindowv2"
|
|
||||||
EventTypeOpenLayer EventType = "openlayer"
|
EventTypeOpenLayer EventType = "openlayer"
|
||||||
EventTypeCloseLayer EventType = "closelayer"
|
EventTypeCloseLayer EventType = "closelayer"
|
||||||
EventTypeOpenWindow EventType = "openwindow"
|
EventTypeOpenWindow EventType = "openwindow"
|
||||||
@ -42,16 +40,12 @@ const (
|
|||||||
EventTypeUrgent EventType = "urgent"
|
EventTypeUrgent EventType = "urgent"
|
||||||
EventTypeMinimize EventType = "minimize"
|
EventTypeMinimize EventType = "minimize"
|
||||||
EventTypeMonitorAdded EventType = "monitoradded"
|
EventTypeMonitorAdded EventType = "monitoradded"
|
||||||
EventTypeMonitorAddedV2 EventType = "monitoraddedv2"
|
|
||||||
EventTypeMonitorRemoved EventType = "monitorremoved"
|
EventTypeMonitorRemoved EventType = "monitorremoved"
|
||||||
EventTypeCreateWorkspace EventType = "createworkspace"
|
EventTypeCreateWorkspace EventType = "createworkspace"
|
||||||
EventTypeCreateWorkspaceV2 EventType = "createworkspacev2"
|
|
||||||
EventTypeDestroyWorkspace EventType = "destroyworkspace"
|
EventTypeDestroyWorkspace EventType = "destroyworkspace"
|
||||||
EventTypeDestroyWorkspaceV2 EventType = "destroyworkspacev2"
|
|
||||||
EventTypeFullscreen EventType = "fullscreen"
|
EventTypeFullscreen EventType = "fullscreen"
|
||||||
EventTypeChangeFloatingMode EventType = "changefloatingmode"
|
EventTypeChangeFloatingMode EventType = "changefloatingmode"
|
||||||
EventTypeWorkspace EventType = "workspace"
|
EventTypeWorkspace EventType = "workspace"
|
||||||
EventTypeWorkspaceV2 EventType = "workspacev2"
|
|
||||||
EventTypeSubmap EventType = "submap"
|
EventTypeSubmap EventType = "submap"
|
||||||
EventTypeMouseMove EventType = "mousemove"
|
EventTypeMouseMove EventType = "mousemove"
|
||||||
EventTypeMouseButton EventType = "mousebutton"
|
EventTypeMouseButton EventType = "mousebutton"
|
||||||
|
@ -5,21 +5,21 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Instance struct {
|
||||||
Signature string `json:"instance"`
|
Signature string `json:"instance"`
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
WaylandSocket string `json:"wl_socket"`
|
WaylandSocket string `json:"wl_socket"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) String() string {
|
func (i Instance) String() string {
|
||||||
data, _ := json.MarshalIndent(c, "", "\t")
|
data, _ := json.MarshalIndent(i, "", "\t")
|
||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) SocketPath() string {
|
func (i Instance) SocketPath() string {
|
||||||
return fmt.Sprintf("/run/user/1000/hypr/%s/.socket.sock", c.Signature)
|
return fmt.Sprintf("/tmp/hypr/%s/.socket.sock", i.Signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) EventSocketPath() string {
|
func (i Instance) EventSocketPath() string {
|
||||||
return fmt.Sprintf("/run/user/1000/hypr/%s/.socket2.sock", c.Signature)
|
return fmt.Sprintf("/tmp/hypr/%s/.socket2.sock", i.Signature)
|
||||||
}
|
}
|
||||||
|
@ -3,39 +3,30 @@ package hypr
|
|||||||
import "encoding/json"
|
import "encoding/json"
|
||||||
|
|
||||||
type Window struct {
|
type Window struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Mapped bool `json:"mapped"`
|
Mapped bool `json:"mapped"`
|
||||||
Hidden bool `json:"hidden"`
|
Hidden bool `json:"hidden"`
|
||||||
At [2]int `json:"at"`
|
At [2]int `json:"at"`
|
||||||
Size [2]int `json:"size"`
|
Size [2]int `json:"size"`
|
||||||
Workspace WorkspaceIdent `json:"workspace"`
|
Workspace WorkspaceIdent `json:"workspace"`
|
||||||
Floating bool `json:"floating"`
|
Floating bool `json:"floating"`
|
||||||
MonitorID int `json:"monitor"`
|
MonitorID int `json:"monitor"`
|
||||||
Class string `json:"class"`
|
Class string `json:"class"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
InitialClass string `json:"initialClass"`
|
InitialClass string `json:"initialClass"`
|
||||||
InitialTitle string `json:"initialTitle"`
|
InitialTitle string `json:"initialTitle"`
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
Xwayland bool `json:"xwayland"`
|
Xwayland bool `json:"xwayland"`
|
||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
Fullscreen FullscreenState `json:"fullscreen"`
|
Fullscreen bool `json:"fullscreen"`
|
||||||
FullscreenClient FullscreenState `json:"fullscreenClient"`
|
FullscreenMode int `json:"fullscreenMode"`
|
||||||
Grouped []interface{} `json:"grouped"` // TODO
|
FakeFullscreen bool `json:"fakeFullscreen"`
|
||||||
Swallowing string `json:"swallowing"`
|
Grouped []interface{} `json:"grouped"` // TODO
|
||||||
FocusHistoryID int `json:"focusHistoryID"`
|
Swallowing string `json:"swallowing"`
|
||||||
|
FocusHistoryID int `json:"focusHistoryID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w Window) String() string {
|
func (w Window) String() string {
|
||||||
data, _ := json.MarshalIndent(w, "", "\t")
|
data, _ := json.MarshalIndent(w, "", "\t")
|
||||||
return string(data)
|
return string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullscreenState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
FullscreenCurrent FullscreenState = -1
|
|
||||||
FullscreenNone FullscreenState = 0
|
|
||||||
FullscreenMaximize FullscreenState = 1
|
|
||||||
FullscreenFullscreen FullscreenState = 2
|
|
||||||
FullscreenMaximizeFullscreen FullscreenState = 3
|
|
||||||
)
|
|
||||||
|
@ -2,13 +2,12 @@ package hypr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.milar.in/milarin/slices"
|
"git.milar.in/milarin/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i *Client) Subscribe(ctx context.Context, events ...EventType) (<-chan Event, error) {
|
func (i *Instance) Subscribe(events ...EventType) (<-chan Event, error) {
|
||||||
r, err := readSocketRaw(i.EventSocketPath(), strings.NewReader(""))
|
r, err := readSocketRaw(i.EventSocketPath(), strings.NewReader(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -16,17 +15,15 @@ func (i *Client) Subscribe(ctx context.Context, events ...EventType) (<-chan Eve
|
|||||||
|
|
||||||
out := make(chan Event, 10)
|
out := make(chan Event, 10)
|
||||||
eventMap := slices.ToStructMap(events)
|
eventMap := slices.ToStructMap(events)
|
||||||
allEvents := len(events) == 0
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
defer close(out)
|
defer close(out)
|
||||||
|
|
||||||
sc := bufio.NewScanner(r)
|
sc := bufio.NewScanner(r)
|
||||||
|
for sc.Scan() {
|
||||||
for ctx.Err() == nil && sc.Scan() {
|
|
||||||
event := parseEvent(sc.Text())
|
event := parseEvent(sc.Text())
|
||||||
if _, ok := eventMap[event.Type]; allEvents || ok {
|
if _, ok := eventMap[event.Type]; ok {
|
||||||
out <- event
|
out <- event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
utils.go
22
utils.go
@ -1,11 +1,9 @@
|
|||||||
package hypr
|
package hypr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func readSocketRaw(socket string, body io.Reader) (io.ReadCloser, error) {
|
func readSocketRaw(socket string, body io.Reader) (io.ReadCloser, error) {
|
||||||
@ -51,23 +49,3 @@ func readSocket[T any](socket string, body io.Reader) (T, error) {
|
|||||||
|
|
||||||
return *value, nil
|
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