diff --git a/server/internal/desktop/manager.go b/server/internal/desktop/manager.go index 51fdbbc5..bd310a7e 100644 --- a/server/internal/desktop/manager.go +++ b/server/internal/desktop/manager.go @@ -6,6 +6,7 @@ import ( "time" "m1k1o/neko/internal/config" + "m1k1o/neko/internal/desktop/xevent" "m1k1o/neko/internal/desktop/xorg" "m1k1o/neko/internal/types" @@ -40,6 +41,17 @@ func (manager *DesktopManagerCtx) Start() { Str("screen_size", fmt.Sprintf("%dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)). Msgf("setting initial screen size") + go xevent.EventLoop(manager.config.Display) + + manager.OnEventError(func(error_code uint8, message string, request_code uint8, minor_code uint8) { + manager.logger.Warn(). + Uint8("error_code", error_code). + Str("message", message). + Uint8("request_code", request_code). + Uint8("minor_code", minor_code). + Msg("X event error occurred") + }) + manager.wg.Add(1) go func() { diff --git a/server/internal/desktop/xevent.go b/server/internal/desktop/xevent.go new file mode 100644 index 00000000..b6603874 --- /dev/null +++ b/server/internal/desktop/xevent.go @@ -0,0 +1,33 @@ +package desktop + +import "m1k1o/neko/internal/desktop/xevent" + +func (manager *DesktopManagerCtx) OnCursorChanged(listener func(serial uint64)) { + xevent.Emmiter.On("cursor-changed", func(payload ...any) { + listener(payload[0].(uint64)) + }) +} + +func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) { + xevent.Emmiter.On("clipboard-updated", func(payload ...any) { + listener() + }) +} + +func (manager *DesktopManagerCtx) OnFileChooserDialogOpened(listener func()) { + xevent.Emmiter.On("file-chooser-dialog-opened", func(payload ...any) { + listener() + }) +} + +func (manager *DesktopManagerCtx) OnFileChooserDialogClosed(listener func()) { + xevent.Emmiter.On("file-chooser-dialog-closed", func(payload ...any) { + listener() + }) +} + +func (manager *DesktopManagerCtx) OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) { + xevent.Emmiter.On("event-error", func(payload ...any) { + listener(payload[0].(uint8), payload[1].(string), payload[2].(uint8), payload[3].(uint8)) + }) +} diff --git a/server/internal/desktop/xevent/xevent.c b/server/internal/desktop/xevent/xevent.c new file mode 100644 index 00000000..3791375c --- /dev/null +++ b/server/internal/desktop/xevent/xevent.c @@ -0,0 +1,123 @@ +#include "xevent.h" + +static int XEventError(Display *display, XErrorEvent *event) { + char message[100]; + + int error; + error = XGetErrorText(display, event->error_code, message, sizeof(message)); + if (error) { + goXEventError(event, "Could not get error message."); + } else { + goXEventError(event, message); + } + + return 1; +} + +void XEventLoop(char *name) { + Display *display = XOpenDisplay(name); + Window root = RootWindow(display, 0); + + int xfixes_event_base, xfixes_error_base; + if (!XFixesQueryExtension(display, &xfixes_event_base, &xfixes_error_base)) { + return; + } + + Atom WM_WINDOW_ROLE = XInternAtom(display, "WM_WINDOW_ROLE", 1); + Atom XA_CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0); + XFixesSelectSelectionInput(display, root, XA_CLIPBOARD, XFixesSetSelectionOwnerNotifyMask); + XFixesSelectCursorInput(display, root, XFixesDisplayCursorNotifyMask); + XSelectInput(display, root, SubstructureNotifyMask); + + XSync(display, 0); + XSetErrorHandler(XEventError); + + while (goXEventActive()) { + XEvent event; + XNextEvent(display, &event); + + // XFixesDisplayCursorNotify + if (event.type == xfixes_event_base + 1) { + XFixesCursorNotifyEvent notifyEvent = *((XFixesCursorNotifyEvent *) &event); + if (notifyEvent.subtype == XFixesDisplayCursorNotify) { + goXEventCursorChanged(notifyEvent); + continue; + } + } + + // XFixesSelectionNotifyEvent + if (event.type == xfixes_event_base + XFixesSelectionNotify) { + XFixesSelectionNotifyEvent notifyEvent = *((XFixesSelectionNotifyEvent *) &event); + if (notifyEvent.subtype == XFixesSetSelectionOwnerNotify && notifyEvent.selection == XA_CLIPBOARD) { + goXEventClipboardUpdated(); + continue; + } + } + + // ConfigureNotify + if (event.type == ConfigureNotify) { + Window window = event.xconfigure.window; + + char *name; + XFetchName(display, window, &name); + + XTextProperty role; + XGetTextProperty(display, window, &role, WM_WINDOW_ROLE); + + goXEventConfigureNotify(display, window, name, role.value); + XFree(name); + continue; + } + + // UnmapNotify + if (event.type == UnmapNotify) { + Window window = event.xunmap.window; + goXEventUnmapNotify(window); + continue; + } + } + + XCloseDisplay(display); +} + +void XFileChooserHide(Display *display, Window window) { + Window root = RootWindow(display, 0); + + // The WM_TRANSIENT_FOR property is defined by the [ICCCM] for managed windows. + // This specification extends the use of the property to override-redirect windows. + // If an override-redirect is a pop-up on behalf of another window, then the Client + // SHOULD set WM_TRANSIENT_FOR on the override-redirect to this other window. + // + // As an example, a Client should set WM_TRANSIENT_FOR on dropdown menus to the + // toplevel application window that contains the menubar. + + // Remove WM_TRANSIENT_FOR + Atom WM_TRANSIENT_FOR = XInternAtom(display, "WM_TRANSIENT_FOR", 0); + XDeleteProperty(display, window, WM_TRANSIENT_FOR); + + // Add _NET_WM_STATE_BELOW + XClientMessageEvent clientMessageEvent; + memset(&clientMessageEvent, 0, sizeof(clientMessageEvent)); + + // window = the respective client window + // message_type = _NET_WM_STATE + // format = 32 + // data.l[0] = the action, as listed below + // _NET_WM_STATE_REMOVE 0 // remove/unset property + // _NET_WM_STATE_ADD 1 // add/set property + // _NET_WM_STATE_TOGGLE 2 // toggle property + // data.l[1] = first property to alter + // data.l[2] = second property to alter + // data.l[3] = source indication + // other data.l[] elements = 0 + + clientMessageEvent.type = ClientMessage; + clientMessageEvent.window = window; + clientMessageEvent.message_type = XInternAtom(display, "_NET_WM_STATE", 0); + clientMessageEvent.format = 32; + clientMessageEvent.data.l[0] = 1; + clientMessageEvent.data.l[1] = XInternAtom(display, "_NET_WM_STATE_BELOW", 0); + clientMessageEvent.data.l[3] = 1; + + XSendEvent(display, root, 0, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent *)&clientMessageEvent); +} diff --git a/server/internal/desktop/xevent/xevent.go b/server/internal/desktop/xevent/xevent.go new file mode 100644 index 00000000..fc91e919 --- /dev/null +++ b/server/internal/desktop/xevent/xevent.go @@ -0,0 +1,84 @@ +package xevent + +/* +#cgo LDFLAGS: -lX11 -lXfixes + +#include "xevent.h" +*/ +import "C" + +import ( + "strings" + "time" + "unsafe" + + "github.com/kataras/go-events" +) + +var Emmiter events.EventEmmiter +var file_chooser_dialog_window uint32 = 0 + +func init() { + Emmiter = events.New() +} + +func EventLoop(display string) { + displayUnsafe := C.CString(display) + defer C.free(unsafe.Pointer(displayUnsafe)) + + C.XEventLoop(displayUnsafe) +} + +//export goXEventCursorChanged +func goXEventCursorChanged(event C.XFixesCursorNotifyEvent) { + Emmiter.Emit("cursor-changed", uint64(event.cursor_serial)) +} + +//export goXEventClipboardUpdated +func goXEventClipboardUpdated() { + Emmiter.Emit("clipboard-updated") +} + +//export goXEventConfigureNotify +func goXEventConfigureNotify(display *C.Display, window C.Window, name *C.char, role *C.char) { + if C.GoString(role) != "GtkFileChooserDialog" { + return + } + + // TODO: Refactor. Right now processing of this dialog relies on identifying + // via its name. When that changes to role, this condition should be removed. + if !strings.HasPrefix(C.GoString(name), "Open File") { + return + } + + C.XFileChooserHide(display, window) + + // Because first dialog is not put properly to background + time.Sleep(10 * time.Millisecond) + C.XFileChooserHide(display, window) + + if file_chooser_dialog_window == 0 { + file_chooser_dialog_window = uint32(window) + Emmiter.Emit("file-chooser-dialog-opened") + } +} + +//export goXEventUnmapNotify +func goXEventUnmapNotify(window C.Window) { + if uint32(window) != file_chooser_dialog_window { + return + } + + file_chooser_dialog_window = 0 + Emmiter.Emit("file-chooser-dialog-closed") +} + +//export goXEventError +func goXEventError(event *C.XErrorEvent, message *C.char) { + Emmiter.Emit("event-error", uint8(event.error_code), C.GoString(message), uint8(event.request_code), uint8(event.minor_code)) +} + +//export goXEventActive +func goXEventActive() C.int { + return C.int(1) +} diff --git a/server/internal/desktop/xevent/xevent.h b/server/internal/desktop/xevent/xevent.h new file mode 100644 index 00000000..3dfdfc0b --- /dev/null +++ b/server/internal/desktop/xevent/xevent.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +extern void goXEventCursorChanged(XFixesCursorNotifyEvent event); +extern void goXEventClipboardUpdated(); +extern void goXEventConfigureNotify(Display *display, Window window, char *name, char *role); +extern void goXEventUnmapNotify(Window window); +extern void goXEventError(XErrorEvent *event, char *message); +extern int goXEventActive(); + +static int XEventError(Display *display, XErrorEvent *event); +void XEventLoop(char *display); + +void XFileChooserHide(Display *display, Window window); diff --git a/server/internal/types/desktop.go b/server/internal/types/desktop.go index c11b218c..4414bbae 100644 --- a/server/internal/types/desktop.go +++ b/server/internal/types/desktop.go @@ -61,4 +61,11 @@ type DesktopManager interface { GetKeyboardModifiers() KeyboardModifiers GetCursorImage() *CursorImage GetScreenshotImage() *image.RGBA + + // xevent + OnCursorChanged(listener func(serial uint64)) + OnClipboardUpdated(listener func()) + OnFileChooserDialogOpened(listener func()) + OnFileChooserDialogClosed(listener func()) + OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) }