From ea5517b270da2c95d32926a3a1ab4b4e885fd351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Thu, 17 Aug 2023 16:14:59 +0200 Subject: [PATCH] Xorg input driver (#53) * add xf86 input driver. * cleanup. * rewrite to unix socket PoC. * add input rebuild. * lint & docs. * add input driver struct. * comments, lint, socket name from config. * add touch events to webrtc. * switch to uint32. * misc update logging & linting, * fix screen size * set touchscreen as core pointer. * add touch to ws control. * SendCoreEvents. * extract to own xinput folder. * add debounce. * switch pressure to uint8. * check buffer size. * send touch events with system init. --- Dockerfile | 46 +- Dockerfile.nvidia | 46 +- dev/rebuild.input | 32 ++ internal/config/desktop.go | 41 +- internal/desktop/manager.go | 59 +- internal/desktop/xinput.go | 36 ++ internal/desktop/xorg.go | 15 +- internal/webrtc/handler.go | 33 ++ internal/webrtc/payload/receive.go | 11 + internal/websocket/handler/control.go | 21 + internal/websocket/handler/handler.go | 16 + internal/websocket/handler/system.go | 1 + pkg/types/desktop.go | 11 + pkg/types/event/events.go | 4 + pkg/types/message/messages.go | 7 + pkg/xinput/dummy.go | 31 ++ pkg/xinput/types.go | 61 +++ pkg/xinput/xinput.go | 122 +++++ pkg/xorg/xorg.go | 16 +- runtime/xorg.conf | 8 + xorg/xf86-input-neko/.gitignore | 67 +++ xorg/xf86-input-neko/80-neko.conf | 9 + xorg/xf86-input-neko/COPYING | 27 + xorg/xf86-input-neko/Dockerfile | 22 + xorg/xf86-input-neko/Makefile.am | 30 ++ xorg/xf86-input-neko/README.md | 19 + xorg/xf86-input-neko/autogen-clean.sh | 8 + xorg/xf86-input-neko/autogen.sh | 22 + xorg/xf86-input-neko/configure.ac | 109 ++++ xorg/xf86-input-neko/m4/.gitkeep | 0 xorg/xf86-input-neko/release.sh | 121 +++++ xorg/xf86-input-neko/src/Makefile.am | 31 ++ xorg/xf86-input-neko/src/neko.c | 502 ++++++++++++++++++ xorg/xf86-input-neko/xorg-neko.pc.in | 5 + .../xf86-video-dummy}/xdummy-randr.patch | 0 35 files changed, 1507 insertions(+), 82 deletions(-) create mode 100755 dev/rebuild.input create mode 100644 internal/desktop/xinput.go create mode 100644 pkg/xinput/dummy.go create mode 100644 pkg/xinput/types.go create mode 100644 pkg/xinput/xinput.go create mode 100644 xorg/xf86-input-neko/.gitignore create mode 100644 xorg/xf86-input-neko/80-neko.conf create mode 100644 xorg/xf86-input-neko/COPYING create mode 100644 xorg/xf86-input-neko/Dockerfile create mode 100644 xorg/xf86-input-neko/Makefile.am create mode 100644 xorg/xf86-input-neko/README.md create mode 100755 xorg/xf86-input-neko/autogen-clean.sh create mode 100755 xorg/xf86-input-neko/autogen.sh create mode 100644 xorg/xf86-input-neko/configure.ac create mode 100644 xorg/xf86-input-neko/m4/.gitkeep create mode 100755 xorg/xf86-input-neko/release.sh create mode 100644 xorg/xf86-input-neko/src/Makefile.am create mode 100644 xorg/xf86-input-neko/src/neko.c create mode 100644 xorg/xf86-input-neko/xorg-neko.pc.in rename {runtime => xorg/xf86-video-dummy}/xdummy-randr.patch (100%) diff --git a/Dockerfile b/Dockerfile index 1c8db9a6..f40e5e87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,36 @@ # -# Stage 0: Build xserver-xorg-video-dummy 0.3.8-2 with RandR support. +# Stage 0: Build xorg dependencies. # -FROM debian:bullseye-slim as xserver-xorg-video-dummy - -WORKDIR /usr/local/src +FROM debian:bullseye-slim as xorg-deps ENV DEBIAN_FRONTEND=noninteractive + RUN set -eux; \ - cp /etc/apt/sources.list /etc/apt/sources.list~; \ - sed -Ei 's/^deb /deb-src /' /etc/apt/sources.list; \ - cat /etc/apt/sources.list~ >> /etc/apt/sources.list; \ apt-get update; \ - apt-get install -y dpkg-dev git; \ - apt-get build-dep -y xserver-xorg-video-dummy; \ - git clone --depth 1 --branch xserver-xorg-video-dummy-1_0.3.8-2 https://salsa.debian.org/xorg-team/driver/xserver-xorg-video-dummy; \ - # - # clean up - apt-get clean -y; \ - rm -rf /var/lib/apt/lists/* /var/cache/apt/* + apt-get install -y \ + git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \ + && rm -rf /var/lib/apt/lists/*; -COPY runtime/xdummy-randr.patch /tmp/xdummy-randr.patch +WORKDIR /xorg +COPY xorg/ /xorg/ + +# build xserver-xorg-video-dummy 0.3.8-2 with RandR support. RUN set -eux; \ + cd xf86-video-dummy; \ + git clone --depth 1 --branch xserver-xorg-video-dummy-1_0.3.8-2 https://salsa.debian.org/xorg-team/driver/xserver-xorg-video-dummy; \ cd xserver-xorg-video-dummy; \ - patch -p1 < /tmp/xdummy-randr.patch; \ - bash ./autogen.sh; \ - make; \ + patch -p1 < ../xdummy-randr.patch; \ + ./autogen.sh; \ + make -j$(nproc); \ + make install; + +# build custom input driver +RUN set -eux; \ + cd xf86-input-neko; \ + ./autogen.sh --prefix=/usr; \ + ./configure; \ + make -j$(nproc); \ make install; # @@ -129,8 +134,9 @@ RUN set -eux; \ apt-get clean -y; \ rm -rf /var/lib/apt/lists/* /var/cache/apt/* -# replace version -COPY --from=xserver-xorg-video-dummy /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so +# copy dependencies from previous stage +COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so +COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so # # copy runtime configs diff --git a/Dockerfile.nvidia b/Dockerfile.nvidia index 1bccff48..fe977365 100644 --- a/Dockerfile.nvidia +++ b/Dockerfile.nvidia @@ -52,33 +52,38 @@ RUN set -eux; \ meson install -C build; # -# Stage 0: Build xserver-xorg-video-dummy 0.3.8-2 with RandR support. +# Stage 0: Build xorg dependencies. # -FROM debian:bullseye-slim as xserver-xorg-video-dummy - -WORKDIR /usr/local/src +FROM debian:bullseye-slim as xorg-deps ENV DEBIAN_FRONTEND=noninteractive + RUN set -eux; \ - cp /etc/apt/sources.list /etc/apt/sources.list~; \ - sed -Ei 's/^deb /deb-src /' /etc/apt/sources.list; \ - cat /etc/apt/sources.list~ >> /etc/apt/sources.list; \ apt-get update; \ - apt-get install -y dpkg-dev git; \ - apt-get build-dep -y xserver-xorg-video-dummy; \ - git clone --depth 1 --branch xserver-xorg-video-dummy-1_0.3.8-2 https://salsa.debian.org/xorg-team/driver/xserver-xorg-video-dummy; \ - # - # clean up - apt-get clean -y; \ - rm -rf /var/lib/apt/lists/* /var/cache/apt/* + apt-get install -y \ + git gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \ + && rm -rf /var/lib/apt/lists/*; -COPY runtime/xdummy-randr.patch /tmp/xdummy-randr.patch +WORKDIR /xorg +COPY xorg/ /xorg/ + +# build xserver-xorg-video-dummy 0.3.8-2 with RandR support. RUN set -eux; \ + cd xf86-video-dummy; \ + git clone --depth 1 --branch xserver-xorg-video-dummy-1_0.3.8-2 https://salsa.debian.org/xorg-team/driver/xserver-xorg-video-dummy; \ cd xserver-xorg-video-dummy; \ - patch -p1 < /tmp/xdummy-randr.patch; \ - bash ./autogen.sh; \ - make; \ + patch -p1 < ../xdummy-randr.patch; \ + ./autogen.sh; \ + make -j$(nproc); \ + make install; + +# build custom input driver +RUN set -eux; \ + cd xf86-input-neko; \ + ./autogen.sh --prefix=/usr; \ + ./configure; \ + make -j$(nproc); \ make install; # @@ -236,8 +241,9 @@ RUN set -eux; \ apt-get clean -y; \ rm -rf /var/lib/apt/lists/* /var/cache/apt/* -# replace version -COPY --from=xserver-xorg-video-dummy /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so +# copy dependencies from previous stage +COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so +COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so # # configure EGL and Vulkan manually diff --git a/dev/rebuild.input b/dev/rebuild.input new file mode 100755 index 00000000..7f10523d --- /dev/null +++ b/dev/rebuild.input @@ -0,0 +1,32 @@ +#!/bin/bash +cd "$(dirname "$0")" +cd ../xorg/xf86-input-neko + +# +# aborting if any command returns a non-zero value +set -e + +# +# check if docker image exists +if [ -z "$(docker images -q xf86-input-neko)" ]; then + echo "Docker image not found, building it" + docker build -t xf86-input-neko . +fi + +# +# if there is no ./configure script, run autogen.sh and configure +if [ ! -f ./configure ]; then + docker run -v $PWD/:/app --rm xf86-input-neko bash -c './autogen.sh && ./configure' +fi + +# +# make install +docker run -v $PWD/:/app --rm xf86-input-neko bash -c 'make && make install DESTDIR=/app/build' + +# +# replace input driver in container +docker cp "${PWD}/build/usr/local/lib/xorg/modules/input/neko_drv.so" neko_server_dev:/usr/lib/xorg/modules/input/neko_drv.so + +# +# restart server +docker exec neko_server_dev supervisorctl -c /etc/neko/supervisord.conf restart x-server diff --git a/internal/config/desktop.go b/internal/config/desktop.go index 4a21674d..a18d2c12 100644 --- a/internal/config/desktop.go +++ b/internal/config/desktop.go @@ -7,16 +7,19 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/demodesk/neko/pkg/types" ) type Desktop struct { Display string - Unminimize bool + ScreenSize types.ScreenSize - ScreenWidth int - ScreenHeight int - ScreenRate int16 + UseInputDriver bool + InputSocket string + + Unminimize bool } func (Desktop) Init(cmd *cobra.Command) error { @@ -25,6 +28,16 @@ func (Desktop) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("desktop.input.enabled", true, "whether custom xf86 input driver should be used to handle touchscreen") + if err := viper.BindPFlag("desktop.input.enabled", cmd.PersistentFlags().Lookup("desktop.input.enabled")); err != nil { + return err + } + + cmd.PersistentFlags().String("desktop.input.socket", "/tmp/xf86-input-neko.sock", "socket path for custom xf86 input driver connection") + if err := viper.BindPFlag("desktop.input.socket", cmd.PersistentFlags().Lookup("desktop.input.socket")); err != nil { + return err + } + cmd.PersistentFlags().Bool("desktop.unminimize", true, "automatically unminimize window when it is minimized") if err := viper.BindPFlag("desktop.unminimize", cmd.PersistentFlags().Lookup("desktop.unminimize")); err != nil { return err @@ -37,11 +50,11 @@ func (s *Desktop) Set() { // Display is provided by env variable s.Display = os.Getenv("DISPLAY") - s.Unminimize = viper.GetBool("desktop.unminimize") - - s.ScreenWidth = 1280 - s.ScreenHeight = 720 - s.ScreenRate = 30 + s.ScreenSize = types.ScreenSize{ + Width: 1280, + Height: 720, + Rate: 30, + } r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`) res := r.FindStringSubmatch(viper.GetString("desktop.screen")) @@ -52,9 +65,13 @@ func (s *Desktop) Set() { rate, err3 := strconv.ParseInt(res[3], 10, 64) if err1 == nil && err2 == nil && err3 == nil { - s.ScreenWidth = int(width) - s.ScreenHeight = int(height) - s.ScreenRate = int16(rate) + s.ScreenSize.Width = int(width) + s.ScreenSize.Height = int(height) + s.ScreenSize.Rate = int16(rate) } } + + s.UseInputDriver = viper.GetBool("desktop.input.enabled") + s.InputSocket = viper.GetString("desktop.input.socket") + s.Unminimize = viper.GetBool("desktop.unminimize") } diff --git a/internal/desktop/manager.go b/internal/desktop/manager.go index dff9b9f6..2f981618 100644 --- a/internal/desktop/manager.go +++ b/internal/desktop/manager.go @@ -1,7 +1,6 @@ package desktop import ( - "fmt" "sync" "time" @@ -10,26 +9,39 @@ import ( "github.com/rs/zerolog/log" "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/pkg/types" "github.com/demodesk/neko/pkg/xevent" + "github.com/demodesk/neko/pkg/xinput" "github.com/demodesk/neko/pkg/xorg" ) var mu = sync.Mutex{} type DesktopManagerCtx struct { - logger zerolog.Logger - wg sync.WaitGroup - shutdown chan struct{} - emmiter events.EventEmmiter - config *config.Desktop + logger zerolog.Logger + wg sync.WaitGroup + shutdown chan struct{} + emmiter events.EventEmmiter + config *config.Desktop + screenSize types.ScreenSize // cached screen size + input xinput.Driver } func New(config *config.Desktop) *DesktopManagerCtx { + var input xinput.Driver + if config.UseInputDriver { + input = xinput.NewDriver(config.InputSocket) + } else { + input = xinput.NewDummy() + } + return &DesktopManagerCtx{ - logger: log.With().Str("module", "desktop").Logger(), - shutdown: make(chan struct{}), - emmiter: events.New(), - config: config, + logger: log.With().Str("module", "desktop").Logger(), + shutdown: make(chan struct{}), + emmiter: events.New(), + config: config, + screenSize: config.ScreenSize, + input: input, } } @@ -40,10 +52,24 @@ func (manager *DesktopManagerCtx) Start() { xorg.GetScreenConfigurations() - width, height, rate, err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) - manager.logger.Err(err). - Str("screen_size", fmt.Sprintf("%dx%d@%d", width, height, rate)). - Msgf("setting initial screen size") + screenSize, err := xorg.ChangeScreenSize(manager.config.ScreenSize) + if err != nil { + manager.logger.Err(err). + Str("screen_size", screenSize.String()). + Msgf("unable to set initial screen size") + } else { + // cache screen size + manager.screenSize = screenSize + manager.logger.Info(). + Str("screen_size", screenSize.String()). + Msgf("setting initial screen size") + } + + err = manager.input.Connect() + if err != nil { + // TODO: fail silently to dummy driver? + manager.logger.Panic().Err(err).Msg("unable to connect to input driver") + } xevent.Unminimize = manager.config.Unminimize go xevent.EventLoop(manager.config.Display) @@ -68,12 +94,15 @@ func (manager *DesktopManagerCtx) Start() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() + const debounceDuration = 10 * time.Second + for { select { case <-manager.shutdown: return case <-ticker.C: - xorg.CheckKeys(time.Second * 10) + xorg.CheckKeys(debounceDuration) + manager.input.Debounce(debounceDuration) } } }() diff --git a/internal/desktop/xinput.go b/internal/desktop/xinput.go new file mode 100644 index 00000000..69f78c60 --- /dev/null +++ b/internal/desktop/xinput.go @@ -0,0 +1,36 @@ +package desktop + +import "github.com/demodesk/neko/pkg/xinput" + +func (manager *DesktopManagerCtx) inputRelToAbs(x, y int) (int, int) { + return (x * xinput.AbsX) / manager.screenSize.Width, (y * xinput.AbsY) / manager.screenSize.Height +} + +func (manager *DesktopManagerCtx) HasTouchSupport() bool { + // we assume now, that if the input driver is enabled, we have touch support + return manager.config.UseInputDriver +} + +func (manager *DesktopManagerCtx) TouchBegin(touchId uint32, x, y int, pressure uint8) error { + mu.Lock() + defer mu.Unlock() + + x, y = manager.inputRelToAbs(x, y) + return manager.input.TouchBegin(touchId, x, y, pressure) +} + +func (manager *DesktopManagerCtx) TouchUpdate(touchId uint32, x, y int, pressure uint8) error { + mu.Lock() + defer mu.Unlock() + + x, y = manager.inputRelToAbs(x, y) + return manager.input.TouchUpdate(touchId, x, y, pressure) +} + +func (manager *DesktopManagerCtx) TouchEnd(touchId uint32, x, y int, pressure uint8) error { + mu.Lock() + defer mu.Unlock() + + x, y = manager.inputRelToAbs(x, y) + return manager.input.TouchEnd(touchId, x, y, pressure) +} diff --git a/internal/desktop/xorg.go b/internal/desktop/xorg.go index 917f467d..867a775f 100644 --- a/internal/desktop/xorg.go +++ b/internal/desktop/xorg.go @@ -85,7 +85,7 @@ func (manager *DesktopManagerCtx) ScreenConfigurations() []types.ScreenSize { return configs } -func (manager *DesktopManagerCtx) SetScreenSize(size types.ScreenSize) (types.ScreenSize, error) { +func (manager *DesktopManagerCtx) SetScreenSize(screenSize types.ScreenSize) (types.ScreenSize, error) { mu.Lock() manager.emmiter.Emit("before_screen_size_change") @@ -94,12 +94,13 @@ func (manager *DesktopManagerCtx) SetScreenSize(size types.ScreenSize) (types.Sc mu.Unlock() }() - w, h, r, err := xorg.ChangeScreenSize(size.Width, size.Height, size.Rate) - return types.ScreenSize{ - Width: w, - Height: h, - Rate: r, - }, err + screenSize, err := xorg.ChangeScreenSize(screenSize) + if err == nil { + // cache the new screen size + manager.screenSize = screenSize + } + + return screenSize, err } func (manager *DesktopManagerCtx) GetScreenSize() types.ScreenSize { diff --git a/internal/webrtc/handler.go b/internal/webrtc/handler.go index 7d828f2a..00f0fcd9 100644 --- a/internal/webrtc/handler.go +++ b/internal/webrtc/handler.go @@ -151,6 +151,39 @@ func (manager *WebRTCManagerCtx) handle( } else { logger.Trace().Uint32("key", payload.Key).Msg("button up") } + case payload.OP_TOUCH_BEGIN: + payload := &payload.Touch{} + if err := binary.Read(buffer, binary.BigEndian, payload); err != nil { + return err + } + + if err := manager.desktop.TouchBegin(payload.TouchId, int(payload.X), int(payload.Y), payload.Pressure); err != nil { + logger.Warn().Err(err).Uint32("touchId", payload.TouchId).Msg("touch begin failed") + } else { + logger.Trace().Uint32("touchId", payload.TouchId).Msg("touch begin") + } + case payload.OP_TOUCH_UPDATE: + payload := &payload.Touch{} + if err := binary.Read(buffer, binary.BigEndian, payload); err != nil { + return err + } + + if err := manager.desktop.TouchUpdate(payload.TouchId, int(payload.X), int(payload.Y), payload.Pressure); err != nil { + logger.Warn().Err(err).Uint32("touchId", payload.TouchId).Msg("touch update failed") + } else { + logger.Trace().Uint32("touchId", payload.TouchId).Msg("touch update") + } + case payload.OP_TOUCH_END: + payload := &payload.Touch{} + if err := binary.Read(buffer, binary.BigEndian, payload); err != nil { + return err + } + + if err := manager.desktop.TouchEnd(payload.TouchId, int(payload.X), int(payload.Y), payload.Pressure); err != nil { + logger.Warn().Err(err).Uint32("touchId", payload.TouchId).Msg("touch end failed") + } else { + logger.Trace().Uint32("touchId", payload.TouchId).Msg("touch end") + } } return nil diff --git a/internal/webrtc/payload/receive.go b/internal/webrtc/payload/receive.go index 68ef8f77..37383d7e 100644 --- a/internal/webrtc/payload/receive.go +++ b/internal/webrtc/payload/receive.go @@ -10,6 +10,10 @@ const ( OP_BTN_DOWN = 0x05 OP_BTN_UP = 0x06 OP_PING = 0x07 + // touch events + OP_TOUCH_BEGIN = 0x08 + OP_TOUCH_UPDATE = 0x09 + OP_TOUCH_END = 0x0a ) type Move struct { @@ -35,3 +39,10 @@ type Ping struct { func (p Ping) ClientTs() uint64 { return (uint64(p.ClientTs1) * uint64(math.MaxUint32)) + uint64(p.ClientTs2) } + +type Touch struct { + TouchId uint32 + X int32 + Y int32 + Pressure uint8 +} diff --git a/internal/websocket/handler/control.go b/internal/websocket/handler/control.go index 2eec4a3b..456d166b 100644 --- a/internal/websocket/handler/control.go +++ b/internal/websocket/handler/control.go @@ -155,6 +155,27 @@ func (h *MessageHandlerCtx) controlKeyUp(session types.Session, payload *message return h.desktop.KeyUp(payload.Keysym) } +func (h *MessageHandlerCtx) controlTouchBegin(session types.Session, payload *message.ControlTouch) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + return h.desktop.TouchBegin(payload.TouchId, payload.X, payload.Y, payload.Pressure) +} + +func (h *MessageHandlerCtx) controlTouchUpdate(session types.Session, payload *message.ControlTouch) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + return h.desktop.TouchUpdate(payload.TouchId, payload.X, payload.Y, payload.Pressure) +} + +func (h *MessageHandlerCtx) controlTouchEnd(session types.Session, payload *message.ControlTouch) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + return h.desktop.TouchEnd(payload.TouchId, payload.X, payload.Y, payload.Pressure) +} + func (h *MessageHandlerCtx) controlCut(session types.Session) error { if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { return err diff --git a/internal/websocket/handler/handler.go b/internal/websocket/handler/handler.go index d42863a9..8e8a8566 100644 --- a/internal/websocket/handler/handler.go +++ b/internal/websocket/handler/handler.go @@ -122,6 +122,22 @@ func (h *MessageHandlerCtx) Message(session types.Session, data types.WebSocketM err = utils.Unmarshal(payload, data.Payload, func() error { return h.controlKeyUp(session, payload) }) + // touch + case event.CONTROL_TOUCHBEGIN: + payload := &message.ControlTouch{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlTouchBegin(session, payload) + }) + case event.CONTROL_TOUCHUPDATE: + payload := &message.ControlTouch{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlTouchUpdate(session, payload) + }) + case event.CONTROL_TOUCHEND: + payload := &message.ControlTouch{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlTouchEnd(session, payload) + }) // actions case event.CONTROL_CUT: err = h.controlCut(session) diff --git a/internal/websocket/handler/system.go b/internal/websocket/handler/system.go index 09318909..adf375d2 100644 --- a/internal/websocket/handler/system.go +++ b/internal/websocket/handler/system.go @@ -47,6 +47,7 @@ func (h *MessageHandlerCtx) systemInit(session types.Session) error { ScreenSize: screenSize, Sessions: sessions, Settings: h.sessions.Settings(), + TouchEvents: h.desktop.HasTouchSupport(), ScreencastEnabled: h.capture.Screencast().Enabled(), WebRTC: message.SystemWebRTC{ Videos: h.capture.Video().IDs(), diff --git a/pkg/types/desktop.go b/pkg/types/desktop.go index 24e694b0..16e36468 100644 --- a/pkg/types/desktop.go +++ b/pkg/types/desktop.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "image" ) @@ -19,6 +20,10 @@ type ScreenSize struct { Rate int16 } +func (s ScreenSize) String() string { + return fmt.Sprintf("%dx%d@%d", s.Width, s.Height, s.Rate) +} + type KeyboardModifiers struct { NumLock *bool CapsLock *bool @@ -68,6 +73,12 @@ type DesktopManager interface { OnFileChooserDialogClosed(listener func()) OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) + // input driver + HasTouchSupport() bool + TouchBegin(touchId uint32, x, y int, pressure uint8) error + TouchUpdate(touchId uint32, x, y int, pressure uint8) error + TouchEnd(touchId uint32, x, y int, pressure uint8) error + // clipboard ClipboardGetText() (*ClipboardText, error) ClipboardSetText(data ClipboardText) error diff --git a/pkg/types/event/events.go b/pkg/types/event/events.go index 55e34f41..32857b9e 100644 --- a/pkg/types/event/events.go +++ b/pkg/types/event/events.go @@ -43,6 +43,10 @@ const ( CONTROL_KEYPRESS = "control/keypress" CONTROL_KEYDOWN = "control/keydown" CONTROL_KEYUP = "control/keyup" + // touch + CONTROL_TOUCHBEGIN = "control/touchbegin" + CONTROL_TOUCHUPDATE = "control/touchupdate" + CONTROL_TOUCHEND = "control/touchend" // actions CONTROL_CUT = "control/cut" CONTROL_COPY = "control/copy" diff --git a/pkg/types/message/messages.go b/pkg/types/message/messages.go index 1bded7b4..b3bea68b 100644 --- a/pkg/types/message/messages.go +++ b/pkg/types/message/messages.go @@ -20,6 +20,7 @@ type SystemInit struct { ScreenSize ScreenSize `json:"screen_size"` Sessions map[string]SessionData `json:"sessions"` Settings types.Settings `json:"settings"` + TouchEvents bool `json:"touch_events"` ScreencastEnabled bool `json:"screencast_enabled"` WebRTC SystemWebRTC `json:"webrtc"` } @@ -129,6 +130,12 @@ type ControlKey struct { Keysym uint32 `json:"keysym"` } +type ControlTouch struct { + TouchId uint32 `json:"touch_id"` + *ControlPos + Pressure uint8 `json:"pressure"` +} + ///////////////////////////// // Screen ///////////////////////////// diff --git a/pkg/xinput/dummy.go b/pkg/xinput/dummy.go new file mode 100644 index 00000000..a2d5c344 --- /dev/null +++ b/pkg/xinput/dummy.go @@ -0,0 +1,31 @@ +package xinput + +import "time" + +type dummy struct{} + +func NewDummy() Driver { + return &dummy{} +} + +func (d *dummy) Connect() error { + return nil +} + +func (d *dummy) Close() error { + return nil +} + +func (d *dummy) Debounce(duration time.Duration) {} + +func (d *dummy) TouchBegin(touchId uint32, x, y int, pressure uint8) error { + return nil +} + +func (d *dummy) TouchUpdate(touchId uint32, x, y int, pressure uint8) error { + return nil +} + +func (d *dummy) TouchEnd(touchId uint32, x, y int, pressure uint8) error { + return nil +} diff --git a/pkg/xinput/types.go b/pkg/xinput/types.go new file mode 100644 index 00000000..7f94044d --- /dev/null +++ b/pkg/xinput/types.go @@ -0,0 +1,61 @@ +package xinput + +import "time" + +const ( + // absolute coordinates used in driver + AbsX = 0xffff + AbsY = 0xffff +) + +const ( + XI_TouchBegin = 18 + XI_TouchUpdate = 19 + XI_TouchEnd = 20 +) + +type Message struct { + _type uint16 + touchId uint32 + x int32 // can be negative? + y int32 // can be negative? + pressure uint8 +} + +func (msg *Message) Unpack(buffer []byte) { + msg._type = uint16(buffer[0]) + msg.touchId = uint32(buffer[1]) | (uint32(buffer[2]) << 8) + msg.x = int32(buffer[3]) | (int32(buffer[4]) << 8) | (int32(buffer[5]) << 16) | (int32(buffer[6]) << 24) + msg.y = int32(buffer[7]) | (int32(buffer[8]) << 8) | (int32(buffer[9]) << 16) | (int32(buffer[10]) << 24) + msg.pressure = uint8(buffer[11]) +} + +func (msg *Message) Pack() []byte { + var buffer [12]byte + + buffer[0] = byte(msg._type) + buffer[1] = byte(msg.touchId) + buffer[2] = byte(msg.touchId >> 8) + buffer[3] = byte(msg.x) + buffer[4] = byte(msg.x >> 8) + buffer[5] = byte(msg.x >> 16) + buffer[6] = byte(msg.x >> 24) + buffer[7] = byte(msg.y) + buffer[8] = byte(msg.y >> 8) + buffer[9] = byte(msg.y >> 16) + buffer[10] = byte(msg.y >> 24) + buffer[11] = byte(msg.pressure) + + return buffer[:] +} + +type Driver interface { + Connect() error + Close() error + // release touches, that were not updated for duration + Debounce(duration time.Duration) + // touch events + TouchBegin(touchId uint32, x, y int, pressure uint8) error + TouchUpdate(touchId uint32, x, y int, pressure uint8) error + TouchEnd(touchId uint32, x, y int, pressure uint8) error +} diff --git a/pkg/xinput/xinput.go b/pkg/xinput/xinput.go new file mode 100644 index 00000000..a6ce1033 --- /dev/null +++ b/pkg/xinput/xinput.go @@ -0,0 +1,122 @@ +/* custom xf86 input driver communication protocol */ +package xinput + +import ( + "fmt" + "net" + "sync" + "time" +) + +type driver struct { + mu sync.Mutex + socket string + conn net.Conn + + debounceTouchIds map[uint32]time.Time +} + +func NewDriver(socket string) Driver { + return &driver{ + socket: socket, + + debounceTouchIds: make(map[uint32]time.Time), + } +} + +func (d *driver) Connect() error { + c, err := net.Dial("unix", d.socket) + if err != nil { + return err + } + d.conn = c + return nil +} + +func (d *driver) Close() error { + return d.conn.Close() +} + +func (d *driver) Debounce(duration time.Duration) { + d.mu.Lock() + defer d.mu.Unlock() + + t := time.Now() + for touchId, start := range d.debounceTouchIds { + if t.Sub(start) < duration { + continue + } + + msg := Message{ + _type: XI_TouchEnd, + touchId: touchId, + x: -1, + y: -1, + } + _, _ = d.conn.Write(msg.Pack()) + delete(d.debounceTouchIds, touchId) + } +} + +func (d *driver) TouchBegin(touchId uint32, x, y int, pressure uint8) error { + d.mu.Lock() + defer d.mu.Unlock() + + if _, ok := d.debounceTouchIds[touchId]; ok { + return fmt.Errorf("debounced touch id %v", touchId) + } + + d.debounceTouchIds[touchId] = time.Now() + + msg := Message{ + _type: XI_TouchBegin, + touchId: touchId, + x: int32(x), + y: int32(y), + pressure: pressure, + } + _, err := d.conn.Write(msg.Pack()) + return err +} + +func (d *driver) TouchUpdate(touchId uint32, x, y int, pressure uint8) error { + d.mu.Lock() + defer d.mu.Unlock() + + if _, ok := d.debounceTouchIds[touchId]; !ok { + return fmt.Errorf("unknown touch id %v", touchId) + } + + d.debounceTouchIds[touchId] = time.Now() + + msg := Message{ + _type: XI_TouchUpdate, + touchId: touchId, + x: int32(x), + y: int32(y), + pressure: pressure, + } + _, err := d.conn.Write(msg.Pack()) + return err +} + +func (d *driver) TouchEnd(touchId uint32, x, y int, pressure uint8) error { + d.mu.Lock() + defer d.mu.Unlock() + + if _, ok := d.debounceTouchIds[touchId]; !ok { + return fmt.Errorf("unknown touch id %v", touchId) + } + + delete(d.debounceTouchIds, touchId) + + msg := Message{ + _type: XI_TouchEnd, + touchId: touchId, + x: int32(x), + y: int32(y), + pressure: pressure, + } + _, err := d.conn.Write(msg.Pack()) + return err +} diff --git a/pkg/xorg/xorg.go b/pkg/xorg/xorg.go index 2be47a66..b9846974 100644 --- a/pkg/xorg/xorg.go +++ b/pkg/xorg/xorg.go @@ -185,20 +185,20 @@ func CheckKeys(duration time.Duration) { } // set screen configuration, create new one if not exists -func ChangeScreenSize(width int, height int, rate int16) (int, int, int16, error) { +func ChangeScreenSize(s types.ScreenSize) (types.ScreenSize, error) { mu.Lock() defer mu.Unlock() // round width to 8, because of Xorg - width = width - (width % 8) + s.Width = s.Width - (s.Width % 8) // if rate is 0, set it to 60 - if rate == 0 { - rate = 60 + if s.Rate == 0 { + s.Rate = 60 } // convert variables to C types - c_width, c_height, c_rate := C.int(width), C.int(height), C.short(rate) + c_width, c_height, c_rate := C.int(s.Width), C.int(s.Height), C.short(s.Rate) // if screen configuration already exists, just set it status := C.XSetScreenConfiguration(c_width, c_height, c_rate) @@ -214,15 +214,15 @@ func ChangeScreenSize(width int, height int, rate int16) (int, int, int16, error // if screen configuration was not set successfully, return error if status != C.RRSetConfigSuccess { - err = fmt.Errorf("unknown screen configuration %dx%d@%d", width, height, rate) + err = fmt.Errorf("unknown screen configuration %s", s.String()) } // if specified rate is not supported a BadValue error is returned if status == C.BadValue { - err = fmt.Errorf("unsupported screen rate %d", rate) + err = fmt.Errorf("unsupported screen rate %d", s.Rate) } - return width, height, rate, err + return s, err } func GetScreenSize() types.ScreenSize { diff --git a/runtime/xorg.conf b/runtime/xorg.conf index 188d5a9e..ed0e8488 100644 --- a/runtime/xorg.conf +++ b/runtime/xorg.conf @@ -23,6 +23,13 @@ Section "InputDevice" Driver "void" EndSection +Section "InputDevice" + Identifier "dummy_touchscreen" + Option "SendCoreEvents" "On" + Option "SocketName" "/tmp/xf86-input-neko.sock" + Driver "neko" +EndSection + Section "Device" Identifier "dummy_videocard" Driver "dummy" @@ -107,4 +114,5 @@ Section "ServerLayout" Screen "dummy_screen" InputDevice "dummy_mouse" InputDevice "dummy_keyboard" + InputDevice "dummy_touchscreen" "CorePointer" EndSection diff --git a/xorg/xf86-input-neko/.gitignore b/xorg/xf86-input-neko/.gitignore new file mode 100644 index 00000000..2cb08241 --- /dev/null +++ b/xorg/xf86-input-neko/.gitignore @@ -0,0 +1,67 @@ +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su + +# generated files +aclocal.m4 +config.h.in +config.guess +configure +config.sub +depcomp +install-sh +ltmain.sh +Makefile +Makefile.in +src/Makefile +src/Makefile.in +missing +autom4te.cache/ +compile +config.h +config.h.in~ +config.log +config.status +libtool +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 +src/.deps/ +stamp-h1 +src/.libs/ +*.pc + +# build folder +build/ diff --git a/xorg/xf86-input-neko/80-neko.conf b/xorg/xf86-input-neko/80-neko.conf new file mode 100644 index 00000000..29a315bf --- /dev/null +++ b/xorg/xf86-input-neko/80-neko.conf @@ -0,0 +1,9 @@ +# +# Create new Xorg input device for Neko +# + +Section "InputDevice" + Identifier "dummy_touchscreen" + Option "SocketName" "/tmp/xf86-input-neko.sock" + Driver "neko" +EndSection diff --git a/xorg/xf86-input-neko/COPYING b/xorg/xf86-input-neko/COPYING new file mode 100644 index 00000000..668f45b2 --- /dev/null +++ b/xorg/xf86-input-neko/COPYING @@ -0,0 +1,27 @@ +The MIT License + +Copyright 1999 Frederic Lepied, France +Copyright 2005 Adam Jackson +Copyright 2005 Sun Microsystems, Inc. +Copyright 2006 Sascha Hauer, Pengutronix +Copyright 2007 Clement Chauplannaz, Thales e-Transactions +Copyright 2017 Martin Kepplinger +Copyright 2023 Miroslav Sedivy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/xorg/xf86-input-neko/Dockerfile b/xorg/xf86-input-neko/Dockerfile new file mode 100644 index 00000000..4eb666e0 --- /dev/null +++ b/xorg/xf86-input-neko/Dockerfile @@ -0,0 +1,22 @@ +FROM debian:bullseye-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN set -eux; \ + apt-get update; \ + apt-get install -y \ + gcc pkgconf autoconf automake libtool make xorg-dev xutils-dev \ + && rm -rf /var/lib/apt/lists/*; + +WORKDIR /app + +COPY ./ /app/ + +RUN set -eux; \ + ./autogen.sh --prefix=/usr; \ + ./configure; \ + make -j$(nproc); \ + make install; + +# docker build -t xf86-input-neko . +# docker run -v $PWD/build:/app/build --rm xf86-input-neko make install DESTDIR=/app/build diff --git a/xorg/xf86-input-neko/Makefile.am b/xorg/xf86-input-neko/Makefile.am new file mode 100644 index 00000000..215bddac --- /dev/null +++ b/xorg/xf86-input-neko/Makefile.am @@ -0,0 +1,30 @@ +# Copyright 2005 Adam Jackson. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# on the rights to use, copy, modify, merge, publish, distribute, sub +# license, and/or sell copies of the Software, and to permit persons to whom +# the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +# ADAM JACKSON BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +AUTOMAKE_OPTIONS = foreign +SUBDIRS = src +ACLOCAL_AMFLAGS = -I m4 + +pkgconfigdir = $(libdir)/pkgconfig +pkgconfig_DATA = xorg-neko.pc + +dist_xorgconf_DATA = 80-neko.conf + +EXTRA_DIST = README.md diff --git a/xorg/xf86-input-neko/README.md b/xorg/xf86-input-neko/README.md new file mode 100644 index 00000000..1deace7b --- /dev/null +++ b/xorg/xf86-input-neko/README.md @@ -0,0 +1,19 @@ +# xf86-input-neko +[X.org](https://x.org/) [neko](http://github.com/demodesk/neko) input driver + +### how to use +xf86-input-neko assumes you have only one virtual touchscreen device available, see +`80-neko.conf`. If there are multiple in your system, please specify one config +section for each. +xf86-input-neko aims to make [neko](http://github.com/demodesk/neko) easy to use and doesn't +offer special configuration options. + +* `./configure --prefix=/usr` +* `make` +* `sudo make install` + +Done. + +To _uninstall_, again go inside the extracted directory, and do + + sudo make uninstall diff --git a/xorg/xf86-input-neko/autogen-clean.sh b/xorg/xf86-input-neko/autogen-clean.sh new file mode 100755 index 00000000..12588880 --- /dev/null +++ b/xorg/xf86-input-neko/autogen-clean.sh @@ -0,0 +1,8 @@ +#!/bin/sh +if [ -f Makefile ]; then + echo "Making make distclean..." + make distclean +fi +echo "Removing autogenned files..." +rm -f config.guess config.sub configure install-sh missing mkinstalldirs Makefile.in ltmain.sh stamp-h.in */Makefile.in ltconfig stamp-h config.h.in* aclocal.m4 compile depcomp +echo "Done." diff --git a/xorg/xf86-input-neko/autogen.sh b/xorg/xf86-input-neko/autogen.sh new file mode 100755 index 00000000..4d7b2f31 --- /dev/null +++ b/xorg/xf86-input-neko/autogen.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +autoreconf -f -i -I $(pwd)/m4 +exit $? + +echo -n "Libtoolize..." +libtoolize --force --copy +echo "Done." +echo -n "Aclocal..." +aclocal +echo "Done." +echo -n "Autoheader..." +autoheader +echo "Done." +echo -n "Automake..." +automake --add-missing --copy +echo "Done." +echo -n "Autoconf..." +autoconf +echo "Done." +#./configure $* +echo "Now you can do ./configure, make, make install." diff --git a/xorg/xf86-input-neko/configure.ac b/xorg/xf86-input-neko/configure.ac new file mode 100644 index 00000000..98e9aeeb --- /dev/null +++ b/xorg/xf86-input-neko/configure.ac @@ -0,0 +1,109 @@ +# Copyright 2005 Adam Jackson. +# Copyright 2017 Martin Kepplinger. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# on the rights to use, copy, modify, merge, publish, distribute, sub +# license, and/or sell copies of the Software, and to permit persons to whom +# the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +# ADAM JACKSON BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Process this file with autoconf to produce a configure script + +AC_PREREQ(2.59) +AC_INIT([xf86-input-neko],[0.0.1],[https://github.com/demodesk/neko/issues],[xf86-input-neko],[https://github.com/demodesk/neko]) + +AC_CONFIG_SRCDIR([Makefile.am]) +AC_CONFIG_AUX_DIR(.) +AC_CONFIG_MACRO_DIR([m4]) +AM_INIT_AUTOMAKE([dist-bzip2 dist-xz]) + +DRIVER_NAME=neko +AC_SUBST([DRIVER_NAME]) + +AC_CONFIG_HEADERS(config.h) + +# Checks for programs. +AC_DISABLE_STATIC +AC_PROG_LIBTOOL +AC_PROG_CC + +# Initialize libtool +LT_INIT + +m4_ifndef([XORG_MACROS_VERSION], + [m4_fatal([you must install xorg-macros before running autoconf/autogen. + Hint: either install from source, git://anongit.freedesktop.org/xorg/util/macros or, + depending on you distribution, try package 'xutils-dev' or 'xorg-x11-util-macros'])]) +XORG_DEFAULT_OPTIONS + +AH_TOP([#include "xorg-server.h"]) + +#AC_DEFINE(XFree86LOADER,1,[Stub define for loadable drivers]) +# +#AC_ARG_ENABLE(XINPUT, AS_HELP_STRING([--enable-xinput], +# [Build XInput support (default: yes)]), +# [XINPUT=$enableval],[XINPUT=yes]) +#AM_CONDITIONAL(XINPUT, test "x$XINPUT" = "xyes") +#if test "x$XINPUT" = "xyes" ; then +# AC_DEFINE(XINPUT,1,[Enable XInput support]) +#fi +# +#AC_ARG_ENABLE(XKB, AS_HELP_STRING([--enable-xkb], +# [Build XKB support (default: yes)]), +# [XKB=$enableval],[XKB=yes]) +#AM_CONDITIONAL(XKB, test "x$XKB" = "xyes") +#if test "x$XKB" = "xyes" ; then +# AC_DEFINE(XKB,1,[Enable XKB support]) +#fi + +AC_ARG_WITH(xorg-module-dir, + AS_HELP_STRING(--with-xorg-module-dir=DIR,Default xorg module directory [[default=$libdir/xorg/modules]]), + [moduledir="$withval"], + [moduledir="$libdir/xorg/modules"]) +inputdir=${moduledir}/input +AC_SUBST(inputdir) + +AC_ARG_WITH(xorg-conf-dir, + AC_HELP_STRING([--with-xorg-conf-dir=DIR], + [Default xorg.conf.d directory [[default=${prefix}/share/X11/xorg.conf.d/]]]), + [xorgconfdir="$withval"], + [xorgconfdir='${prefix}/share/X11/xorg.conf.d']) +AC_SUBST(xorgconfdir) + +# Checks for extensions +XORG_DRIVER_CHECK_EXT(RANDR, randrproto) +XORG_DRIVER_CHECK_EXT(XINPUT, inputproto) + +# Checks for pkg-config packages +PKG_CHECK_MODULES(XORG, xorg-server xproto $REQUIRED_MODULES) +sdkdir=$(pkg-config --variable=sdkdir xorg-server) + +CFLAGS="$CFLAGS $XORG_CFLAGS "' -I$(top_srcdir)/src' +CFLAGS="-O2 -Wall -W -fPIC $CFLAGS" +AC_SUBST([CFLAGS]) + +# Checks for libraries. +#AC_CHECK_LIB(ts, ts_open, [], [ +# echo "Error! You need to have libts." +# exit -1 +# ]) + +# Checks for header files. +AC_HEADER_STDC + +AC_CONFIG_FILES([Makefile + src/Makefile + xorg-neko.pc]) +AC_OUTPUT diff --git a/xorg/xf86-input-neko/m4/.gitkeep b/xorg/xf86-input-neko/m4/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/xorg/xf86-input-neko/release.sh b/xorg/xf86-input-neko/release.sh new file mode 100755 index 00000000..02830e0c --- /dev/null +++ b/xorg/xf86-input-neko/release.sh @@ -0,0 +1,121 @@ +#!/bin/bash +: 'Copyright (C) 2017, Martin Kepplinger + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE.' + +# Script to build release-archives with. This requires a checkout from git. +# Before running it, change the version in configure.ac. Only do this. Do +# *not* commit this change. This script will do it. + +# WARNING: This script is very dangerous! It may delete any untracked files. + +have_version=0 + +usage() +{ + echo "Usage: $0 -v version" +} + +args=$(getopt -o v:s -- "$@") +if [ $? -ne 0 ] ; then + usage + exit 1 +fi +eval set -- "$args" +while [ $# -gt 0 ] +do + case "$1" in + -v) + version=$2 + have_version=1 + shift + ;; + --) + shift + break + ;; + *) + echo "Invalid option: $1" + usage + exit 1 + ;; + esac + shift +done + +# Do we have a desired version number? +if [ "$have_version" -gt 0 ] ; then + echo "trying to build version $version" +else + echo "please specify a version" + usage + exit 1 +fi + +# Version number sanity check +if grep ${version} configure.ac +then + echo "configurations seems ok" +else + echo "please check your configure.ac" + exit 1 +fi + +# Check that we are on master +branch=$(git rev-parse --abbrev-ref HEAD) +echo "we are on branch $branch" + +if [ ! "${branch}" = "master" ] ; then + echo "you don't seem to be on the master branch" + exit 1 +fi + +if git diff-index --quiet HEAD --; then + # no changes + echo "there are no uncommitted changes (version bump)" + exit 1 +fi +echo "======================================================" +echo " are you fine with the following version bump?" +echo "======================================================" +git diff +echo "======================================================" +read -p " Press enter to continue" +echo "======================================================" + +./autogen.sh && ./configure && make distcheck +./autogen-clean.sh +git clean -d -f + +git commit -a -m "xf86-input-neko ${version}" +git tag -s ${version} -m "xf86-input-neko ${version}" + +./autogen.sh && ./configure && make distcheck +sha256sum xf86-input-neko-${version}.tar.xz > xf86-input-neko-${version}.tar.xz.sha256 +sha256sum xf86-input-neko-${version}.tar.gz > xf86-input-neko-${version}.tar.gz.sha256 +sha256sum xf86-input-neko-${version}.tar.bz2 > xf86-input-neko-${version}.tar.bz2.sha256 + +sha512sum xf86-input-neko-${version}.tar.xz > xf86-input-neko-${version}.tar.xz.sha512 +sha512sum xf86-input-neko-${version}.tar.gz > xf86-input-neko-${version}.tar.gz.sha512 +sha512sum xf86-input-neko-${version}.tar.bz2 > xf86-input-neko-${version}.tar.bz2.sha512 + +gpg -b -a xf86-input-neko-${version}.tar.xz +gpg -b -a xf86-input-neko-${version}.tar.gz +gpg -b -a xf86-input-neko-${version}.tar.bz2 + diff --git a/xorg/xf86-input-neko/src/Makefile.am b/xorg/xf86-input-neko/src/Makefile.am new file mode 100644 index 00000000..2221bbcc --- /dev/null +++ b/xorg/xf86-input-neko/src/Makefile.am @@ -0,0 +1,31 @@ +# Copyright 2005 Adam Jackson. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# on the rights to use, copy, modify, merge, publish, distribute, sub +# license, and/or sell copies of the Software, and to permit persons to whom +# the Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice (including the next +# paragraph) shall be included in all copies or substantial portions of the +# Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +# ADAM JACKSON BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +# this is obnoxious: +# -module lets us name the module exactly how we want +# -avoid-version prevents gratuitous .0.0.0 version numbers on the end +# _ladir passes a dummy rpath to libtool so the thing will actually link +# TODO: -nostdlib/-Bstatic/-lgcc platform magic, not installing the .a, etc. +@DRIVER_NAME@_drv_la_LTLIBRARIES = @DRIVER_NAME@_drv.la +@DRIVER_NAME@_drv_la_LDFLAGS = -module -avoid-version +@DRIVER_NAME@_drv_ladir = @inputdir@ + +@DRIVER_NAME@_drv_la_SOURCES = @DRIVER_NAME@.c diff --git a/xorg/xf86-input-neko/src/neko.c b/xorg/xf86-input-neko/src/neko.c new file mode 100644 index 00000000..3022d64b --- /dev/null +++ b/xorg/xf86-input-neko/src/neko.c @@ -0,0 +1,502 @@ +/* + * (c) 2017 Martin Kepplinger + * (c) 2007 Clement Chauplannaz, Thales e-Transactions + * (c) 2006 Sascha Hauer, Pengutronix + * (c) 2023 Miroslav Sedivy + * + * derived from the xf86-input-void driver + * Copyright 1999 by Frederic Lepied, France. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation, and that the name of Frederic Lepied not be used in + * advertising or publicity pertaining to distribution of the software without + * specific, written prior permission. Frederic Lepied makes no + * representations about the suitability of this software for any purpose. It + * is provided "as is" without express or implied warranty. + * + * FREDERIC LEPIED DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO + * EVENT SHALL FREDERIC LEPIED BE LIABLE FOR ANY SPECIAL, INDIRECT OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, + * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + * + * SPDX-License-Identifier: MIT + * License-Filename: COPYING + */ + +/* neko input driver */ +// https://www.x.org/releases/X11R7.7/doc/xorg-server/Xinput.html + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define DEF_SOCKET_NAME "/tmp/xf86-input-neko.sock" +#define BUFFER_SIZE 12 + +#include +#include +#include +#include +#include +#include +#if !defined(DGUX) +#include +#endif +#include +#include +#include /* Needed for InitValuator/Proximity stuff */ +#include +#include +#include +#include + +#define MAXBUTTONS 11 /* > 10 */ +#define TOUCH_NUM_AXES 3 /* x, y, pressure */ +#define TOUCH_MAX_SLOTS 10 + +struct neko_message +{ + uint16_t type; + uint32_t touchId; + int32_t x; + int32_t y; + uint8_t pressure; +}; + +struct neko_priv +{ + pthread_t thread; + /* config */ + int height; + int width; + int pmax; + ValuatorMask *valuators; + uint16_t slots; + /* socket */ + struct sockaddr_un addr; + int listen_socket; + char *socket_name; +}; + +// from binary representation to struct +static void +UnpackNekoMessage(struct neko_message *msg, unsigned char *buffer) +{ + msg->type = buffer[0]; // TODO: use full 16bit type + msg->touchId = buffer[1] | (buffer[2] << 8); // TODO: use full 32bit touchId + msg->x = buffer[3] | (buffer[4] << 8) | (buffer[5] << 16) | (buffer[6] << 24); + msg->y = buffer[7] | (buffer[8] << 8) | (buffer[9] << 16) | (buffer[10] << 24); + msg->pressure = buffer[11]; +} + +static void +ReadInput(InputInfoPtr pInfo) +{ + struct neko_priv *priv = (struct neko_priv *) (pInfo->private); + struct neko_message msg; + int ret; + + int data_socket; + unsigned char buffer[BUFFER_SIZE]; + + for (;;) + { + /* Wait for incoming connection. */ + data_socket = accept(priv->listen_socket, NULL, NULL); + + /* Handle error conditions. */ + if (data_socket == -1) + { + xf86IDrvMsg(pInfo, X_ERROR, "unable to accept connection\n"); + break; + } + + xf86IDrvMsg(pInfo, X_INFO, "accepted connection\n"); + + for(;;) + { + /* Wait for next data packet. */ + ret = read(data_socket, buffer, BUFFER_SIZE); + + /* Handle error conditions. */ + if (ret == -1) + { + xf86IDrvMsg(pInfo, X_ERROR, "unable to read data\n"); + break; + } + + /* Connection closed by client. */ + if (ret == 0) + { + xf86IDrvMsg(pInfo, X_INFO, "connection closed\n"); + break; + } + + /* Ensure message is long enough. */ + if (ret != BUFFER_SIZE) + { + xf86IDrvMsg(pInfo, X_ERROR, "invalid message size\n"); + break; + } + + UnpackNekoMessage(&msg, buffer); + + ValuatorMask *m = priv->valuators; + valuator_mask_zero(m); + + // do not send valuators if x and y are -1 + if (msg.x != -1 && msg.y != -1) + { + valuator_mask_set_double(m, 0, msg.x); + valuator_mask_set_double(m, 1, msg.y); + valuator_mask_set_double(m, 2, msg.pressure); + } + + // TODO: extend to other types, such as keyboard and mouse + xf86PostTouchEvent(pInfo->dev, msg.touchId, msg.type, 0, m); + } + + /* Close socket. */ + close(data_socket); + + xf86IDrvMsg(pInfo, X_INFO, "closed connection\n"); + } +} + +static void +PointerCtrl(__attribute__ ((unused)) DeviceIntPtr device, + __attribute__ ((unused)) PtrCtrl *ctrl) +{ +} + +static int +DeviceControl(DeviceIntPtr device, int what) +{ + // device pInfo + InputInfoPtr pInfo = device->public.devicePrivate; + // custom private data + struct neko_priv *priv = pInfo->private; + + switch (what) { + case DEVICE_INIT: + device->public.on = FALSE; + + unsigned char map[MAXBUTTONS + 1]; + Atom labels[MAXBUTTONS]; + Atom axis_labels[TOUCH_NUM_AXES]; + + // init button map + memset(map, 0, sizeof(map)); + for (int i = 0; i < MAXBUTTONS; i++) + { + map[i + 1] = i + 1; + } + + // init labels + memset(labels, 0, ARRAY_SIZE(labels) * sizeof(Atom)); + labels[0] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_LEFT); + labels[1] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_MIDDLE); + labels[2] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_RIGHT); + labels[3] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_WHEEL_UP); + labels[4] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_WHEEL_DOWN); + labels[5] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_HWHEEL_LEFT); + labels[6] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_HWHEEL_RIGHT); + labels[7] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_SIDE); + labels[8] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_EXTRA); + labels[9] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_FORWARD); + labels[10] = XIGetKnownProperty(BTN_LABEL_PROP_BTN_BACK); + + // init axis labels + memset(axis_labels, 0, ARRAY_SIZE(axis_labels) * sizeof(Atom)); + axis_labels[0] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_X); + axis_labels[1] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_Y); + axis_labels[2] = XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_PRESSURE); + + /* initialize mouse emulation valuators */ + if (InitPointerDeviceStruct((DevicePtr)device, + map, + MAXBUTTONS, labels, + PointerCtrl, + GetMotionHistorySize(), + TOUCH_NUM_AXES, axis_labels) == FALSE) + { + xf86IDrvMsg(pInfo, X_ERROR, + "unable to allocate PointerDeviceStruct\n"); + return !Success; + } + + /* + This function is provided to initialize an XAxisInfoRec, and should be + called for core and extension devices that have valuators. The space + for the XAxisInfoRec is allocated by the InitValuatorClassDeviceStruct + function, but is not initialized. + + InitValuatorAxisStruct should be called once for each axis of motion + reported by the device. Each invocation should be passed the axis + number (starting with 0), the minimum value for that axis, the maximum + value for that axis, and the resolution of the device in counts per meter. + If the device reports relative motion, 0 should be reported as the + minimum and maximum values. + + InitValuatorAxisStruct(dev, axnum, minval, maxval, resolution) + DeviceIntPtr dev; + int axnum; + int minval; + int maxval; + int resolution; + */ + xf86InitValuatorAxisStruct(device, 0, + XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_X), + 0, /* min val */ + priv->width - 1, /* max val */ + priv->width, /* resolution */ + 0, /* min_res */ + priv->width, /* max_res */ + Absolute); + + xf86InitValuatorAxisStruct(device, 1, + XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_POSITION_Y), + 0, /* min val */ + priv->height - 1, /* max val */ + priv->height, /* resolution */ + 0, /* min_res */ + priv->height, /* max_res */ + Absolute); + + xf86InitValuatorAxisStruct(device, 2, + XIGetKnownProperty(AXIS_LABEL_PROP_ABS_MT_PRESSURE), + 0, /* min val */ + priv->pmax, /* max val */ + priv->pmax + 1, /* resolution */ + 0, /* min_res */ + priv->pmax + 1, /* max_res */ + Absolute); + + /* + The mode field is either XIDirectTouch for direct−input touch devices + such as touchscreens or XIDependentTouch for indirect input devices such + as touchpads. For XIDirectTouch devices, touch events are sent to window + at the position the touch occured. For XIDependentTouch devices, touch + events are sent to the window at the position of the device's sprite. + + The num_touches field defines the maximum number of simultaneous touches + the device supports. A num_touches of 0 means the maximum number of + simultaneous touches is undefined or unspecified. This field should be + used as a guide only, devices will lie about their capabilities. + */ + if (InitTouchClassDeviceStruct(device, + priv->slots, + XIDirectTouch, + TOUCH_NUM_AXES) == FALSE) + { + xf86IDrvMsg(pInfo, X_ERROR, + "unable to allocate TouchClassDeviceStruct\n"); + return !Success; + } + + break; + + case DEVICE_ON: + xf86IDrvMsg(pInfo, X_INFO, "DEVICE ON\n"); + device->public.on = TRUE; + + if (priv->thread == 0) + { + /* start thread */ + pthread_create(&priv->thread, NULL, (void *)ReadInput, pInfo); + } + break; + + case DEVICE_OFF: + case DEVICE_CLOSE: + xf86IDrvMsg(pInfo, X_INFO, "DEVICE OFF\n"); + device->public.on = FALSE; + break; + } + + return Success; +} + +static int +PreInit(__attribute__ ((unused)) InputDriverPtr drv, + InputInfoPtr pInfo, + __attribute__ ((unused)) int flags) +{ + struct neko_priv *priv; + int ret; + + priv = calloc(1, sizeof (struct neko_priv)); + if (!priv) + { + xf86IDrvMsg(pInfo, X_ERROR, "%s: out of memory\n", __FUNCTION__); + return BadValue; + } + + pInfo->type_name = (char*)XI_TOUCHSCREEN; + pInfo->device_control = DeviceControl; + pInfo->read_input = NULL; + pInfo->control_proc = NULL; + pInfo->switch_mode = NULL; /* Only support Absolute mode */ + pInfo->private = priv; + pInfo->fd = -1; + + /* get socket name from config */ + priv->socket_name = xf86SetStrOption(pInfo->options, "SocketName", DEF_SOCKET_NAME); + + /* + * In case the program exited inadvertently on the last run, + * remove the socket. + */ + + unlink(priv->socket_name); + + /* Create local socket. */ + priv->listen_socket = socket(AF_UNIX, SOCK_STREAM, 0); + if (priv->listen_socket == -1) + { + xf86IDrvMsg(pInfo, X_ERROR, "unable to create socket\n"); + return BadValue; + } + + /* + * For portability clear the whole structure, since some + * implementations have additional (nonstandard) fields in + * the structure. + */ + + memset(&priv->addr, 0, sizeof(struct sockaddr_un)); + + /* Bind socket to socket name. */ + + priv->addr.sun_family = AF_UNIX; + strncpy(priv->addr.sun_path, priv->socket_name, sizeof(priv->addr.sun_path) - 1); + + ret = bind(priv->listen_socket, (const struct sockaddr *) &priv->addr, sizeof(struct sockaddr_un)); + if (ret == -1) + { + xf86IDrvMsg(pInfo, X_ERROR, "unable to bind socket\n"); + return BadValue; + } + + /* + * Prepare for accepting connections. The backlog size is set + * to 5. So while one request is being processed other requests + * can be waiting. + */ + + ret = listen(priv->listen_socket, 5); + if (ret == -1) + { + xf86IDrvMsg(pInfo, X_ERROR, "unable to listen on socket\n"); + return BadValue; + } + + /* process generic options */ + xf86CollectInputOptions(pInfo, NULL); + xf86ProcessCommonOptions(pInfo, pInfo->options); + + /* create valuators */ + priv->valuators = valuator_mask_new(TOUCH_NUM_AXES); + if (!priv->valuators) + { + xf86IDrvMsg(pInfo, X_ERROR, "%s: out of memory\n", __FUNCTION__); + return BadValue; + } + + priv->slots = TOUCH_MAX_SLOTS; + priv->width = 0xffff; + priv->height = 0xffff; + priv->pmax = 255; + priv->thread = 0; + + /* Return the configured device */ + return Success; +} + +static void +UnInit(__attribute__ ((unused)) InputDriverPtr drv, + InputInfoPtr pInfo, + __attribute__ ((unused)) int flags) +{ + struct neko_priv *priv = (struct neko_priv *)(pInfo->private); + + /* close socket */ + close(priv->listen_socket); + /* remove socket file */ + unlink(priv->socket_name); + + if (priv->thread) + { + /* cancel thread */ + pthread_cancel(priv->thread); + /* wait for thread to finish */ + pthread_join(priv->thread, NULL); + /* ensure thread is not cancelled again */ + priv->thread = 0; + } + + valuator_mask_free(&priv->valuators); + DeviceControl(pInfo->dev, DEVICE_OFF); + + free(pInfo->private); + pInfo->private = NULL; + xf86DeleteInput(pInfo, 0); +} + +/** + * X module information and plug / unplug routines + */ + +_X_EXPORT InputDriverRec NEKO = +{ + .driverVersion = 1, + .driverName = "neko", + .Identify = NULL, + .PreInit = PreInit, + .UnInit = UnInit, + .module = NULL +}; + +static pointer +Plug(pointer module, + __attribute__ ((unused)) pointer options, + __attribute__ ((unused)) int *errmaj, + __attribute__ ((unused)) int *errmin) +{ + xf86AddInputDriver(&NEKO, module, 0); + return module; +} + +static void +Unplug(__attribute__ ((unused)) pointer module) +{ +} + +static XF86ModuleVersionInfo versionRec = +{ + .modname = "neko", + .vendor = MODULEVENDORSTRING, + ._modinfo1_ = MODINFOSTRING1, + ._modinfo2_ = MODINFOSTRING2, + .xf86version = XORG_VERSION_CURRENT, + .majorversion = PACKAGE_VERSION_MAJOR, + .minorversion = PACKAGE_VERSION_MINOR, + .patchlevel = PACKAGE_VERSION_PATCHLEVEL, + .abiclass = ABI_CLASS_XINPUT, + .abiversion = ABI_XINPUT_VERSION, + .moduleclass = MOD_CLASS_XINPUT, + .checksum = {0, 0, 0, 0} /* signature, to be patched into the file by a tool */ +}; + +_X_EXPORT XF86ModuleData nekoModuleData = +{ + .vers = &versionRec, + .setup = Plug, + .teardown = Unplug +}; diff --git a/xorg/xf86-input-neko/xorg-neko.pc.in b/xorg/xf86-input-neko/xorg-neko.pc.in new file mode 100644 index 00000000..37dea7e3 --- /dev/null +++ b/xorg/xf86-input-neko/xorg-neko.pc.in @@ -0,0 +1,5 @@ +Name: xorg-neko +Description: X.Org neko input driver. +Version: @PACKAGE_VERSION@ +Libs: -L${libdir} +Cflags: -I${includedir} diff --git a/runtime/xdummy-randr.patch b/xorg/xf86-video-dummy/xdummy-randr.patch similarity index 100% rename from runtime/xdummy-randr.patch rename to xorg/xf86-video-dummy/xdummy-randr.patch