├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── include ├── all.h ├── event.h ├── extensions.h ├── externals.h ├── fake_outputs.h ├── globals.h ├── pointer.h ├── randr.h ├── types.h ├── util.h └── xedgewarp.h ├── man └── xedgewarp.man ├── obj └── .gitkeep ├── src ├── event.c ├── extensions.c ├── fake_outputs.c ├── pointer.c ├── randr.c ├── util.c └── xedgewarp.c └── test ├── run.pl ├── t ├── 33-torus.t ├── 4-warp-mode-relative.t ├── 5-multiple-directions.t ├── 6-prevent-looping.t ├── 7-nearest-output.t ├── basic.t ├── gaps-and-overlay-are-ignored.t └── prevent-subsequent-warps.t └── xewtest.pm /.gitignore: -------------------------------------------------------------------------------- 1 | xedgewarp 2 | obj/*.o 3 | man/*.1 4 | man/*.xml 5 | test/GLOB* 6 | 7 | # Vim (https://www.gitignore.io/api/vim) 8 | [._]*.s[a-w][a-z] 9 | [._]s[a-w][a-z] 10 | Session.vim 11 | .netrwhist 12 | *~ 13 | tags 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | compiler: 3 | - gcc 4 | before_install: 5 | - "echo 'deb http://archive.ubuntu.com/ubuntu/ trusty main universe' | sudo tee /etc/apt/sources.list.d/trusty.list" 6 | - "echo 'APT::Default-Release \"precise\";' | sudo tee /etc/apt/apt.conf.d/default-release" 7 | # - "echo 'deb http://archive.ubuntu.com/ubuntu/ utopic main universe' | sudo tee /etc/apt/sources.list.d/utopic.list" 8 | install: 9 | - sudo apt-get update 10 | # These are needed to avoid some ugly warnings even though they don't affect us 11 | - sudo apt-get install xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x-ttcidfont-conf 12 | # Now we start with what we need to compile the project 13 | - sudo apt-get install asciidoc 14 | - sudo apt-get install -t trusty --no-install-recommends libxcb-util0-dev libxcb-randr0-dev libxcb-icccm4-dev libxcb-xinerama0-dev libxcb1-dev libx11-dev libxi-dev libx11-xcb-dev 15 | # These we need to run the tests 16 | - sudo apt-get install --no-install-recommends libanyevent-perl libanyevent-i3-perl libextutils-pkgconfig-perl xcb-proto cpanminus xvfb xserver-xephyr xauth libinline-perl libxml-simple-perl libmouse-perl libmousex-nativetraits-perl libextutils-depends-perl perl-modules libtest-deep-perl libtest-exception-perl libxml-parser-perl libtest-simple-perl libtest-fatal-perl libdata-dump-perl libtest-differences-perl libxml-tokeparser-perl libtest-use-ok-perl libipc-run-perl 17 | - sudo /bin/sh -c 'cpanm -n -v X11::XCB || true' 18 | - sudo /bin/sh -c 'cpanm -n -v IPC::System::Simple || true' 19 | script: 20 | - "make all" 21 | - "sudo make install" 22 | - "make test" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2015] [Ingo Bürk] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET = xedgewarp 2 | VERSION = 1.2 3 | SDIR = src 4 | IDIR = include 5 | ODIR = obj 6 | 7 | INSTALL = install 8 | PREFIX ?= /usr/local 9 | BINDIR = $(PREFIX)/bin 10 | MANDIR = $(PREFIX)/share/man/man1 11 | 12 | CC = gcc 13 | CFLAGS += -I$(IDIR) 14 | CFLAGS += -std=gnu99 -fcommon 15 | CFLAGS += -Wall -Wundef -Wshadow -Wformat-security 16 | LIBS = $(shell pkg-config --libs xcb xcb-randr xcb-aux x11 x11-xcb xi) 17 | 18 | INCS = $(wildcard $(IDIR)/*.h) 19 | SRCS = $(wildcard $(SDIR)/*.c) 20 | OBJS = $(patsubst %,$(ODIR)/%,$(notdir $(SRCS:.c=.o))) 21 | 22 | MANS = man/xedgewarp.1 23 | 24 | .NOTPARALLEL: 25 | 26 | .PHONY: all 27 | all: clean $(TARGET) mans 28 | 29 | .PHONY: $(TARGET) 30 | $(TARGET): $(OBJS) 31 | $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) $(LIBS) 32 | 33 | $(ODIR)/%.o: $(SDIR)/%.c $(INCS) 34 | $(CC) -D'__VERSION="${VERSION}"' $(CFLAGS) -o $@ -c $< 35 | 36 | .PHONY: install 37 | install: $(TARGET) 38 | $(INSTALL) -Dm 0755 $(TARGET) $(DESTDIR)$(BINDIR)/$(TARGET) 39 | $(INSTALL) -Dm 0644 man/xedgewarp.1 $(DESTDIR)$(MANDIR)/xedgewarp.1 40 | 41 | .PHONY: uninstall 42 | uninstall: 43 | $(RM) $(DESTDIR)$(BINDIR)/$(TARGET) 44 | $(RM) $(DESTDIR)$(MANDIR)/xedgewarp.1 45 | 46 | .PHONY: mans 47 | mans: $(MANS) 48 | 49 | $(MANS): %.1: %.man 50 | a2x --no-xmllint -f manpage $< 51 | 52 | .PHONY: test 53 | test: clean $(TARGET) 54 | @cd test/; \ 55 | xvfb-run ./run.pl; \ 56 | cd ../ 57 | 58 | .PHONY: clean 59 | clean: 60 | $(RM) $(TARGET) $(OBJS) 61 | $(RM) man/*.1 man/*.xml 62 | $(RM) test/GLOB* 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Travis](https://img.shields.io/travis/Airblader/xedgewarp.svg)](https://travis-ci.org/Airblader/xedgewarp) 2 | [![Issues](https://img.shields.io/github/issues/Airblader/xedgewarp.svg)](https://github.com/Airblader/xedgewarp/issues) 3 | [![Forks](https://img.shields.io/github/forks/Airblader/xedgewarp.svg)](https://github.com/Airblader/xedgewarp/network) 4 | [![Stars](https://img.shields.io/github/stars/Airblader/xedgewarp.svg)](https://github.com/Airblader/xedgewarp/stargazers) 5 | 6 | # xedgewarp 7 | 8 | ## About 9 | 10 | xedgewarp is a window manager agnostic standalone tool that warps the mouse pointer between outputs when necessary. 11 | This is useful in setups with multiple screens where the screens are not aligned exactly next to each other or have different resolutions. Consider the following example: 12 | 13 | +-------+ 14 | | >| 15 | | 1 +-------+ 16 | | | | 17 | +-------+ 2 | 18 | | | 19 | +-------+ 20 | 21 | If you move your cursor from left to right towards the edge that is marked with a `>`, the cursor will simply hit the edge and cannot be moved any further (unless your window manager / desktop environment already takes care of it). However, what you might *want* to happen is that the cursor is warped over to the second output. 22 | 23 | If this is something you want, but your setup lacks -- xedgewarp is the tool for you! 24 | 25 | ## Credits 26 | 27 | Having learned a lot by contributing to [i3/i3](https://github.com/i3/i3), this is also where a lot of the credit should go; namely to its author Michael Stapelberg. Credit for X11::XCB, which is used for testing, goes to the same person. 28 | 29 | ## Installation 30 | 31 | ### Arch / Manjaro (AUR) 32 | 33 | xedgewarp is available in the AUR as [xedgewarp-git](https://aur.archlinux.org/packages/xedgewarp-git/). 34 | 35 | ### Manual 36 | 37 | xedgewarp is make-based. Hence, you can clone the git repository and compile and install it via 38 | 39 | ``` 40 | git clone https://github.com/Airblader/xedgewarp 41 | cd xedgewarp 42 | make 43 | sudo make install 44 | ``` 45 | 46 | ### Dependencies 47 | 48 | xedgewarp links with the following libraries: 49 | * [XCB](https://xcb.freedesktop.org/): libxcb-randr, libxcb-util, libxcb 50 | * [X11](https://xorg.freedesktop.org/): libX11-xcb, libX11, libXi 51 | 52 | Additionally a2x (asciidoc) is required as a compile-time dependency. 53 | 54 | ## Usage 55 | 56 | See `man xedgewarp` after installation. 57 | -------------------------------------------------------------------------------- /include/all.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include "externals.h" 5 | 6 | #include "extensions.h" 7 | #include "event.h" 8 | #include "globals.h" 9 | #include "pointer.h" 10 | #include "randr.h" 11 | #include "types.h" 12 | #include "util.h" 13 | #include "xedgewarp.h" 14 | #include "fake_outputs.h" 15 | -------------------------------------------------------------------------------- /include/event.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include 5 | 6 | /** 7 | * Register for the events we need on the given window. 8 | */ 9 | void event_register_window(xcb_window_t window); 10 | 11 | /** 12 | * Register the root window and all its current children. 13 | * This function temporarily grabs the server. 14 | */ 15 | void event_initialize_tree(void); 16 | 17 | /** 18 | * Enter the X event loop. 19 | */ 20 | void event_enter_loop(void); 21 | 22 | /** 23 | * Handle XCB_CREATE_NOTIFY. 24 | * This is used to register our events on the created window. 25 | */ 26 | void event_handle_create_notify(xcb_create_notify_event_t *event); 27 | 28 | /** 29 | * This is called when we receive a RawMotion event. 30 | * It will query the pointer position and figure out whether 31 | * we need to warp the cursor etc. 32 | */ 33 | void event_handle_motion(void); 34 | -------------------------------------------------------------------------------- /include/extensions.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | /** 5 | * Initialize the required X11 extensions. 6 | */ 7 | void extensions_init(void); 8 | -------------------------------------------------------------------------------- /include/externals.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | -------------------------------------------------------------------------------- /include/fake_outputs.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | /** 5 | * Set up fake RandR outputs from a string. 6 | */ 7 | void fake_outputs_create_outputs(char *outputs_str); 8 | 9 | /** 10 | * Open marker windows for the fake outputs. 11 | */ 12 | void fake_outputs_visualize(void); 13 | -------------------------------------------------------------------------------- /include/globals.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include "types.h" 7 | 8 | /** The Xlib display object. */ 9 | extern Display *display; 10 | 11 | /** Our gate to the other side. */ 12 | extern xcb_connection_t *connection; 13 | 14 | /** The root window of this display. */ 15 | extern xcb_window_t root; 16 | 17 | /** The offset of the RandR extension. */ 18 | extern int randr_ext_offset; 19 | 20 | /* The major opcode of the XInput2 extension. */ 21 | extern int xinput_ext_opcode; 22 | 23 | /** The list of RandR outputs. */ 24 | extern struct outputs_head outputs; 25 | 26 | /* Global configuration. */ 27 | extern Config config; 28 | 29 | /* 30 | * We set this whenever a warp has occured so that we 31 | * prevent further warps until the pointer left the 32 | * edge at least once. 33 | */ 34 | extern bool has_warped; 35 | -------------------------------------------------------------------------------- /include/pointer.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include "types.h" 5 | 6 | /** 7 | * Checks whether the given pointer is touching a "dead" border segment. 8 | * A border segment is considered dead when it is not directly neighboring 9 | * another output. 10 | */ 11 | direction_t pointer_touches_border(position_t pointer); 12 | 13 | /** 14 | * Warps the pointer to the output in the given direction. 15 | */ 16 | void pointer_warp_to_adjacent_output(position_t pointer, direction_t direction); 17 | 18 | /** 19 | * Takes the given position and transforms it to the position it should 20 | * have when warped from one output to the other. 21 | */ 22 | position_t pointer_transform_position(position_t pointer, Output *from, Output *to, direction_t direction); 23 | 24 | /** 25 | * Map the pointer to the correct position if it has to be cycled to the far other 26 | * end of all outputs. 27 | */ 28 | position_t pointer_transform_cycled_position(position_t pointer, Output *to, direction_t direction); 29 | -------------------------------------------------------------------------------- /include/randr.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include "types.h" 7 | 8 | /** 9 | * Query RandR outputs. 10 | * 11 | */ 12 | void randr_query_outputs(void); 13 | 14 | /** 15 | * Returns the output that contains this position. 16 | * Returns NULL if the position is not on any output. 17 | */ 18 | Output *randr_get_output_containing(position_t pointer); 19 | 20 | /** 21 | * Returns the next output in the given direction relative to the specified 22 | * output. Returns NULL if no such output exists. 23 | * The given pointer must lie within the given output. 24 | */ 25 | Output *randr_next_output_in_direction(Output *from, position_t pointer, direction_t direction); 26 | 27 | /** 28 | * Returns the next output in the given direction assuming the outputs 29 | * to form a torus shape, i.e., it will actually look on the far opposite side 30 | * of the given direction. 31 | */ 32 | Output *randr_cycle_output_in_direction(position_t pointer, direction_t direction); 33 | -------------------------------------------------------------------------------- /include/types.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct position_t { 9 | uint32_t x; 10 | uint32_t y; 11 | } position_t; 12 | 13 | typedef struct Rect { 14 | uint32_t x; 15 | uint32_t y; 16 | uint32_t width; 17 | uint32_t height; 18 | } Rect; 19 | 20 | /** Defines an output as detected by RandR. */ 21 | typedef struct Output { 22 | xcb_randr_output_t id; 23 | Rect rect; 24 | 25 | TAILQ_ENTRY(Output) outputs; 26 | } Output; 27 | 28 | TAILQ_HEAD(outputs_head, Output) outputs_head; 29 | 30 | typedef enum direction_t { 31 | D_NONE = 0, 32 | D_TOP = 1 << 0, 33 | D_LEFT = 1 << 1, 34 | D_BOTTOM = 1 << 2, 35 | D_RIGHT = 1 << 3 36 | } direction_t; 37 | 38 | typedef enum warp_mode_t { 39 | /* Warp only as far as necessary. */ 40 | WM_CLOSEST = 0, 41 | /* Warp relative to the position before warping. */ 42 | WM_RELATIVE = 1 43 | } warp_mode_t; 44 | 45 | typedef enum torus_mode_t { 46 | TM_NONE = 0, 47 | TM_VERTICAL = 1 << 0, 48 | TM_HORIZONTAL = 1 << 1 49 | } torus_mode_t; 50 | 51 | typedef enum log_level_t { 52 | L_ERROR = 0, 53 | L_DEBUG = 1, 54 | L_TRACE = 2 55 | } log_level_t; 56 | 57 | typedef struct Config { 58 | /* If set, we use fake outputs and disable RandR (for testing purposes). */ 59 | char *fake_outputs; 60 | 61 | /* Defines how the pointer should be warped. */ 62 | warp_mode_t warp_mode; 63 | 64 | /* Connect the far edges of all outputs to form a torus shape. */ 65 | torus_mode_t torus_mode; 66 | 67 | /* How much spam should we generate? */ 68 | log_level_t log_level; 69 | 70 | /* If true, fork on startup */ 71 | bool fork_mode; 72 | } Config; 73 | -------------------------------------------------------------------------------- /include/util.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #define FREE(p) \ 9 | do { \ 10 | if (p != NULL) { \ 11 | free(p); \ 12 | p = NULL; \ 13 | } \ 14 | } while (0) 15 | 16 | #define ABS(x) ((x) < 0) ? -(x) : (x) 17 | #define MIN(a, b) ((a) < (b)) ? (a) : (b) 18 | 19 | #define TLOG(message, ...) \ 20 | do { \ 21 | if (config.log_level >= L_TRACE) \ 22 | printf("[%s:%d] TRACE: " message "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 23 | } while (0) 24 | 25 | #define DLOG(message, ...) \ 26 | do { \ 27 | if (config.log_level >= L_DEBUG) \ 28 | printf("[%s:%d] DEBUG: " message "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 29 | } while (0) 30 | 31 | #define ELOG(message, ...) \ 32 | do { \ 33 | if (config.log_level >= L_ERROR) \ 34 | fprintf(stderr, "[%s:%d] ERROR: " message "\n", __FILE__, __LINE__, ##__VA_ARGS__); \ 35 | } while (0) 36 | 37 | /** 38 | * Closes the X connection and errors out with the given message. 39 | */ 40 | void bail(char *message); 41 | 42 | /** 43 | * Checks the cookie for errors and returns the result. 44 | */ 45 | bool xcb_return_request_check(xcb_void_cookie_t cookie); 46 | -------------------------------------------------------------------------------- /include/xedgewarp.h: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #pragma once 3 | 4 | #include 5 | 6 | /** 7 | * Initialize the X server connection. 8 | */ 9 | void initialize_x11(void); 10 | 11 | /** 12 | * Initialize xedgewarp specific things from the parsed 13 | * config. 14 | */ 15 | void initialize_xedgewarp(void); 16 | 17 | /** 18 | * Called when xedgewarp terminates. 19 | */ 20 | void on_xedgewarp_exit(void); 21 | 22 | /** 23 | * Parse command-line arguments. 24 | */ 25 | void parse_arguments(int argc, char *argv[]); 26 | -------------------------------------------------------------------------------- /man/xedgewarp.man: -------------------------------------------------------------------------------- 1 | xedgewarp(1) 2 | ============ 3 | Ingo Bürk 4 | 5 | == NAME 6 | 7 | xedgewarp - window manager agnostic pointer warping between outputs 8 | 9 | == SYNOPSIS 10 | 11 | xedgewarp [-b] [-m closest|relative] [-l error|debug|trace] [-h] [-v] 12 | 13 | == OPTIONS 14 | 15 | -b:: 16 | Run in background mode, i.e. fork on startup. 17 | 18 | -m closest|relative:: 19 | Specifies how the mouse pointer should be warped. With "closest", the pointer 20 | is warped only as little as necessary to the closest edge on the output. With 21 | "relative", the relative position (e.g., "75% from the top") on the current 22 | output is calculated and then used on the target output. 23 | 24 | -t [none|v|vertical|h|horizontal|both]:: 25 | Connect the far edges of all outputs as if they were to form a torus shape. 26 | This means, for example, that moving the cursor to the far left edge, instead 27 | of staying there, it will be warped to the far right edge of the far right 28 | output. 29 | The argument is optional and if absent, the default is "both". If the flag is 30 | not set at all, the default is "none". "h" and "v" are abbreviations for 31 | "horizontal" and "vertical", respectively. 32 | 33 | -l error|debug|trace:: 34 | Specify the log level. 35 | 36 | -h:: 37 | Display the usage and exit. 38 | 39 | -v:: 40 | Display the version and exit. 41 | 42 | == DESCRIPTION 43 | 44 | When using multiple outputs, this tool will automatically warp the mouse 45 | pointer in case outputs aren't perfectly aligned and the same size. While some 46 | desktop environments may provide this functionality, xedgewarp can offer 47 | it to those running no desktop environment or one that does not offer this 48 | functionality. 49 | 50 | Consider, for example, the output layout below. If you moved your mouse pointer 51 | from left to right to the position marked with ">", it would simply hit a wall 52 | and not move any further. 53 | 54 | With xedgewarp, the pointer will be automatically warped to either position "1" 55 | ("closest" mode) or "2" ("relative" mode). 56 | 57 | ---- 58 | +-----+ 59 | | | 60 | | >| 61 | | | 62 | | +1------+ 63 | | | | 64 | +-----+2 | 65 | | | 66 | | | 67 | | | 68 | +-------+ 69 | ---- 70 | -------------------------------------------------------------------------------- /obj/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Airblader/xedgewarp/e75aca13bfd567eca3e3e4e611fbb71d76a120c7/obj/.gitkeep -------------------------------------------------------------------------------- /src/event.c: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #include "all.h" 3 | 4 | /* Forward declarations */ 5 | static void event_register_window_substructure_notify(xcb_window_t window); 6 | static void event_register_window_motion(xcb_window_t window); 7 | 8 | /* 9 | * Register for events we need on the given window. 10 | */ 11 | void event_register_window(xcb_window_t window) { 12 | event_register_window_substructure_notify(window); 13 | event_register_window_motion(window); 14 | } 15 | 16 | static void event_register_window_substructure_notify(xcb_window_t window) { 17 | DLOG("Setting event mask for window %d", window); 18 | uint32_t mask = XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY; 19 | if (config.fake_outputs != NULL) 20 | mask |= XCB_EVENT_MASK_POINTER_MOTION; 21 | 22 | xcb_void_cookie_t cookie = xcb_change_window_attributes_checked(connection, 23 | window, XCB_CW_EVENT_MASK, (uint32_t[]) { mask }); 24 | xcb_generic_error_t *error = xcb_request_check(connection, cookie); 25 | if (error == NULL) 26 | return; 27 | 28 | /* The window might not exist anymore by now. In this case, we can safely ignore the error. */ 29 | if (error->error_code == XCB_WINDOW) 30 | DLOG("Window does not exist anymore, cannot set event mask on it."); 31 | else 32 | ELOG("Received error %d when trying to register event mask.", error->error_code); 33 | 34 | FREE(error); 35 | } 36 | 37 | /* TODO port this to XCB once xcb-xinput is stable. */ 38 | static void event_register_window_motion(xcb_window_t window) { 39 | /* For tests we rely on the normal pointer motion events. */ 40 | if (config.fake_outputs != NULL) 41 | return; 42 | 43 | /* For XI2, we only need to register events on the root window. */ 44 | if (window != root) 45 | return; 46 | 47 | XIEventMask masks[1]; 48 | unsigned char mask[(XI_LASTEVENT + 7)/8]; 49 | 50 | memset(mask, 0, sizeof(mask)); 51 | XISetMask(mask, XI_RawMotion); 52 | 53 | masks[0].deviceid = XIAllMasterDevices; 54 | masks[0].mask_len = sizeof(mask); 55 | masks[0].mask = mask; 56 | 57 | XISelectEvents(display, window, masks, 1); 58 | XFlush(display); 59 | } 60 | 61 | static void event_initialize_tree_on(xcb_window_t window) { 62 | /* Register the given window. */ 63 | event_register_window(window); 64 | 65 | DLOG("Querying the tree for window %d", window); 66 | xcb_query_tree_reply_t *reply = xcb_query_tree_reply(connection, xcb_query_tree(connection, window), NULL); 67 | if (reply == NULL) 68 | bail("Could not query the tree, bailing out."); 69 | 70 | /* Register all children as well. */ 71 | xcb_window_t *children = xcb_query_tree_children(reply); 72 | for (int i = 0; i < xcb_query_tree_children_length(reply); i++) { 73 | event_initialize_tree_on(children[i]); 74 | } 75 | 76 | FREE(reply); 77 | } 78 | 79 | /* 80 | * Register the root window and all its current children. 81 | * This function temporarily grabs the server. 82 | */ 83 | void event_initialize_tree(void) { 84 | /* We grab the server for this to avoid race conditions. */ 85 | xcb_grab_server(connection); 86 | event_initialize_tree_on(root); 87 | xcb_ungrab_server(connection); 88 | 89 | /* Make sure we push all these requests to X as quickly as possible. */ 90 | xcb_flush(connection); 91 | 92 | DLOG("Tree has been initialized."); 93 | } 94 | 95 | /* 96 | * Enter the X event loop. 97 | */ 98 | void event_enter_loop(void) { 99 | /* This output is important for tests. */ 100 | DLOG("Entering event loop."); 101 | 102 | xcb_generic_event_t *event; 103 | while ((event = xcb_wait_for_event(connection))) { 104 | /* First, we see if this is a RawMotion event. Since Xinput2 uses 105 | * generic events, we need to cast the event and check its details 106 | * rather than using the extension offset. */ 107 | xcb_ge_generic_event_t *generic_event = (xcb_ge_generic_event_t *) event; 108 | if (generic_event->response_type == XCB_GE_GENERIC && 109 | generic_event->extension == xinput_ext_opcode && 110 | generic_event->event_type == XI_RawMotion) { 111 | 112 | event_handle_motion(); 113 | FREE(event); 114 | continue; 115 | } 116 | 117 | /* From here on out we look at the event type itself. */ 118 | int type = event->response_type & ~0x80; 119 | 120 | /* Check if this is a RandR event. */ 121 | if (type == randr_ext_offset + XCB_RANDR_SCREEN_CHANGE_NOTIFY) { 122 | randr_query_outputs(); 123 | FREE(event); 124 | continue; 125 | } 126 | 127 | switch (type) { 128 | case XCB_CREATE_NOTIFY: 129 | event_handle_create_notify((xcb_create_notify_event_t *) event); 130 | break; 131 | case XCB_MOTION_NOTIFY: 132 | event_handle_motion(); 133 | break; 134 | default: 135 | break; 136 | } 137 | 138 | FREE(event); 139 | } 140 | } 141 | 142 | /* 143 | * Handle XCB_CREATE_NOTIFY. 144 | * This is used to register our events on the created window. 145 | */ 146 | void event_handle_create_notify(xcb_create_notify_event_t *event) { 147 | DLOG("Received CreateNotify event for window %d", event->window); 148 | event_register_window(event->window); 149 | } 150 | 151 | /* 152 | * This is called when we receive a RawMotion event. 153 | * It will query the pointer position and figure out whether 154 | * we need to warp the cursor etc. 155 | */ 156 | void event_handle_motion(void) { 157 | xcb_query_pointer_reply_t *reply = xcb_query_pointer_reply(connection, 158 | xcb_query_pointer(connection, root), NULL); 159 | if (reply == NULL) { 160 | ELOG("Failed to query the cursor position, ignoring this event."); 161 | return; 162 | } 163 | 164 | position_t pointer = (position_t) { 165 | .x = reply->root_x, 166 | .y = reply->root_y 167 | }; 168 | 169 | FREE(reply); 170 | 171 | direction_t direction = pointer_touches_border(pointer); 172 | if (direction == D_NONE) 173 | return; 174 | 175 | /* Check if we already warped without the pointer having left dead border 176 | * segments in the meantime. If so, we ignore this. */ 177 | if (has_warped) { 178 | DLOG("Pointer has already been warped, not warping it again."); 179 | return; 180 | } 181 | 182 | DLOG("Touching a dead border segment at %d / %d.", pointer.x, pointer.y); 183 | pointer_warp_to_adjacent_output(pointer, direction); 184 | } 185 | -------------------------------------------------------------------------------- /src/extensions.c: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #include "all.h" 3 | 4 | /* The offset of the RandR extension. */ 5 | int randr_ext_offset = -1; 6 | 7 | /* The major opcode of the Xinput2 extension. */ 8 | int xinput_ext_opcode = -1; 9 | 10 | /* Forward declarations */ 11 | static void extensions_init_randr(void); 12 | static void extensions_init_xinput(void); 13 | 14 | /* 15 | * Initialize the required X11 extensions. 16 | */ 17 | void extensions_init(void) { 18 | extensions_init_randr(); 19 | extensions_init_xinput(); 20 | } 21 | 22 | static void extensions_init_randr(void) { 23 | const xcb_query_extension_reply_t *reply = xcb_get_extension_data(connection, &xcb_randr_id); 24 | if (!reply->present) 25 | bail("Your X server does not support the RandR extension, bailing out."); 26 | 27 | randr_ext_offset = reply->first_event; 28 | 29 | xcb_randr_select_input(connection, root, 30 | XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE | 31 | XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE | 32 | XCB_RANDR_NOTIFY_MASK_CRTC_CHANGE | 33 | XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY); 34 | xcb_flush(connection); 35 | } 36 | 37 | static void extensions_init_xinput(void) { 38 | /* First we check if XInput is available at all. */ 39 | int event, error; 40 | if (!XQueryExtension(display, "XInputExtension", &xinput_ext_opcode, &event, &error)) 41 | bail("XInput extension is not available."); 42 | 43 | /* Now we check if we have the correct version. We need >= 2.2. */ 44 | int major_opcode = 2; 45 | int minor_opcode = 2; 46 | 47 | int result = XIQueryVersion(display, &major_opcode, &minor_opcode); 48 | if (result == BadRequest) 49 | bail("XI2 is not supported or not in a sufficient version."); 50 | else if (result != Success) 51 | bail("Failed to query XI extension. This is a bug."); 52 | } 53 | -------------------------------------------------------------------------------- /src/fake_outputs.c: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #include "all.h" 3 | 4 | /* 5 | * Set up fake RandR outputs from a string. 6 | */ 7 | void fake_outputs_create_outputs(char *outputs_str) { 8 | uint32_t x, y, width, height; 9 | const char *walk = outputs_str; 10 | while (sscanf(walk, "%ux%u+%u+%u", &width, &height, &x, &y) == 4) { 11 | Output *new = calloc(sizeof(Output), 1); 12 | if (new == NULL) 13 | bail("Could not alloc space for fake output, bailing out."); 14 | 15 | new->id = xcb_generate_id(connection); 16 | 17 | new->rect.width = width; 18 | new->rect.height = height; 19 | new->rect.x = x; 20 | new->rect.y = y; 21 | 22 | TAILQ_INSERT_TAIL(&outputs, new, outputs); 23 | DLOG("Added fake output %d (x = %d / y = %d / w = %d / h = %d) to list of outputs.", new->id, 24 | new->rect.x, new->rect.y, new->rect.width, new->rect.height); 25 | 26 | char buf[1024]; 27 | walk += sprintf(buf, "%ux%u+%u+%u", width, height, x, y) + 1; 28 | } 29 | } 30 | 31 | /* 32 | * Opens a new window with the given rect for the geometry. 33 | * Sets override_redirect so that the window manager (if present) does not manage it. 34 | */ 35 | static void fake_outputs_open_window(Rect *rect, uint32_t background, uint32_t border) { 36 | xcb_window_t window = xcb_generate_id(connection); 37 | xcb_create_window(connection, XCB_COPY_FROM_PARENT, window, root, 38 | rect->x + 1, rect->y + 1, rect->width - 2, rect->height - 2, 39 | 1, 40 | XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_WINDOW_CLASS_COPY_FROM_PARENT, 41 | XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_OVERRIDE_REDIRECT, 42 | (uint32_t[]) { background, border, 1 }); 43 | 44 | xcb_map_window(connection, window); 45 | } 46 | 47 | /* 48 | * Open marker windows for the fake outputs. 49 | */ 50 | void fake_outputs_visualize(void) { 51 | if (config.fake_outputs == NULL) { 52 | ELOG("Can only visualize fake outputs when they are set."); 53 | return; 54 | } 55 | 56 | xcb_screen_t *screen = xcb_aux_get_screen(connection, 0); 57 | 58 | Output *current; 59 | TAILQ_FOREACH(current, &outputs, outputs) { 60 | fake_outputs_open_window(&(current->rect), screen->black_pixel, screen->white_pixel); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/pointer.c: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #include "all.h" 3 | 4 | /* 5 | * We set this whenever a warp has occured so that we 6 | * prevent further warps until the pointer left the 7 | * edge at least once. 8 | */ 9 | bool has_warped = false; 10 | 11 | /* 12 | * Returns a bitmask of directions in which the pointer is touching 13 | * the output border. This function does not check whether the 14 | * border segment is "dead". 15 | */ 16 | static direction_t pointer_touches_any_border(position_t pointer) { 17 | Output *output = randr_get_output_containing(pointer); 18 | if (output == NULL) 19 | return D_NONE; 20 | 21 | Rect *rect = &(output->rect); 22 | int result = D_NONE; 23 | 24 | if (pointer.y == rect->y && pointer.x >= rect->x && pointer.x < rect->x + rect->width) 25 | result |= D_TOP; 26 | if (pointer.x == rect->x && pointer.y >= rect->y && pointer.y < rect->y + rect->height) 27 | result |= D_LEFT; 28 | if (pointer.y + 1 == rect->y + rect->height && pointer.x >= rect->x && pointer.x < rect->x + rect->width) 29 | result |= D_BOTTOM; 30 | if (pointer.x + 1 == rect->x + rect->width && pointer.y >= rect->y && pointer.y < rect->y + rect->height) 31 | result |= D_RIGHT; 32 | 33 | /* Remove D_NONE again if we found something. */ 34 | if (result & D_TOP || result & D_LEFT || result & D_BOTTOM || result & D_RIGHT) 35 | result &= ~D_NONE; 36 | 37 | return result; 38 | } 39 | 40 | /* 41 | * Checks whether the given pointer is touching a "dead" border segment. 42 | * A border segment is considered dead when it is not directly neighboring 43 | * another output. 44 | */ 45 | direction_t pointer_touches_border(position_t pointer) { 46 | /* First, we check if the pointer is touching any border of the output it is on, 47 | * whether or not there is a neighboring output. */ 48 | int directions = pointer_touches_any_border(pointer); 49 | 50 | /* Pointer is not on any border, so we can stop looking. */ 51 | if (directions == D_NONE) { 52 | /* The pointer is not on any border segment, so we are eligible 53 | * for warping the next time it touches a dead border segment 54 | * again. 55 | * Note that we do this here for any border as otherwise, we will 56 | * incorrectly reset this after any warp. */ 57 | has_warped = false; 58 | return D_NONE; 59 | } 60 | 61 | /* Otherwise, we need to check if the border segment is "dead", i.e., there is no 62 | * directly neighboring output as in such a case we don't need to do anything. */ 63 | position_t fake_position = pointer; 64 | direction_t direction; 65 | if (directions & D_LEFT || directions & D_RIGHT) { 66 | direction = directions & D_LEFT ? D_LEFT : D_RIGHT; 67 | fake_position.x += direction == D_LEFT ? -1 : 1; 68 | } else if (directions & D_TOP || directions & D_BOTTOM) { 69 | direction = directions & D_TOP ? D_TOP : D_BOTTOM; 70 | fake_position.y += direction == D_TOP ? -1 : 1; 71 | } else { 72 | bail("Congratulations, you found a bug. Please report it!"); 73 | /* Never reached */ 74 | return D_NONE; 75 | } 76 | 77 | return randr_get_output_containing(fake_position) == NULL ? direction : D_NONE; 78 | } 79 | 80 | /* 81 | * Warps the pointer to the output in the given direction. 82 | */ 83 | void pointer_warp_to_adjacent_output(position_t pointer, direction_t direction) { 84 | Output *current = randr_get_output_containing(pointer); 85 | if (current == NULL) { 86 | ELOG("Cannot determine current output."); 87 | return; 88 | } 89 | 90 | Output *output = randr_next_output_in_direction(current, pointer, direction); 91 | position_t target; 92 | if (output == NULL) { 93 | TLOG("At position %d / %d, there is no more output in direction %d.", 94 | pointer.x, pointer.y, direction); 95 | if (config.torus_mode == TM_NONE || 96 | ((direction == D_LEFT || direction == D_RIGHT) && !(config.torus_mode & TM_HORIZONTAL)) || 97 | ((direction == D_TOP || direction == D_BOTTOM) && !(config.torus_mode & TM_VERTICAL))) { 98 | return; 99 | } 100 | 101 | output = randr_cycle_output_in_direction(pointer, direction); 102 | if (output == NULL) { 103 | TLOG("At position %d / %d, we cannot cycle to the output in direction %d.", 104 | pointer.x, pointer.y, direction); 105 | return; 106 | } 107 | 108 | TLOG("Found output %d to cycle to.", output->id); 109 | target = pointer_transform_cycled_position(pointer, output, direction); 110 | } else { 111 | target = pointer_transform_position(pointer, current, output, direction); 112 | } 113 | 114 | /* Let's do the pointer warp, again! */ 115 | xcb_void_cookie_t cookie = xcb_warp_pointer_checked(connection, XCB_NONE, root, 0, 0, 0, 0, 116 | target.x, target.y); 117 | if (!xcb_return_request_check(cookie)) { 118 | ELOG("Failed to warp the pointer"); 119 | return; 120 | } 121 | 122 | /* Store the fact that we warped to prevent further warping for now. */ 123 | has_warped = true; 124 | 125 | DLOG("Successfully warped pointer from %d / %d (%d) to %d / %d (%d)", 126 | pointer.x, pointer.y, current->id, 127 | target.x, target.y, output->id); 128 | } 129 | 130 | /* 131 | * Calculate the target position of the pointer such that it is only warped as little / far as necessary. 132 | * This means the pointer will be moved to the very edge of the target output. 133 | */ 134 | static position_t pointer_transform_position_closest(position_t pointer, Output *from, Output *to, direction_t direction) { 135 | position_t coordinates = { 136 | .x = 0, 137 | .y = 0 138 | }; 139 | 140 | switch (direction) { 141 | case D_TOP: 142 | coordinates.y = to->rect.y + to->rect.height - 1; 143 | coordinates.x = to->rect.x; 144 | if (to->rect.x <= pointer.x) 145 | coordinates.x += to->rect.width - 1; 146 | 147 | break; 148 | case D_LEFT: 149 | coordinates.x = to->rect.x + to->rect.width - 1; 150 | coordinates.y = to->rect.y; 151 | if (to->rect.y <= pointer.y) 152 | coordinates.y += to->rect.height - 1; 153 | 154 | break; 155 | case D_BOTTOM: 156 | coordinates.y = to->rect.y; 157 | coordinates.x = to->rect.x; 158 | if (to->rect.x <= pointer.x) 159 | coordinates.x += to->rect.width - 1; 160 | 161 | break; 162 | case D_RIGHT: 163 | coordinates.x = to->rect.x; 164 | coordinates.y = to->rect.y; 165 | if (to->rect.y <= pointer.y) 166 | coordinates.y += to->rect.height - 1; 167 | 168 | break; 169 | default: 170 | ELOG("Unknown direction %d.", direction); 171 | break; 172 | } 173 | 174 | return coordinates; 175 | } 176 | 177 | /* 178 | * Calculate the target position of the pointer based on its relative position on the source output. 179 | * For example, if the pointer is warped to the right and was located 30% from the top of the source output, 180 | * it will be warped to the position 30% from the top of the target output. 181 | */ 182 | static position_t pointer_transform_position_relative(position_t pointer, Output *from, Output *to, direction_t direction) { 183 | /* To initially get to the correct output, we borrow the logic here and adapt afterwards. */ 184 | position_t coordinates = pointer_transform_position_closest(pointer, from, to, direction); 185 | 186 | float percent; 187 | switch (direction) { 188 | case D_TOP: 189 | case D_BOTTOM: 190 | percent = (pointer.x - from->rect.x) / (float) from->rect.width; 191 | coordinates.x = to->rect.x + percent * to->rect.width; 192 | break; 193 | case D_LEFT: 194 | case D_RIGHT: 195 | percent = (pointer.y - from->rect.y) / (float) from->rect.height; 196 | coordinates.y = to->rect.y + percent * to->rect.height; 197 | break; 198 | default: 199 | ELOG("Unknown direction %d.", direction); 200 | break; 201 | } 202 | 203 | return coordinates; 204 | } 205 | 206 | /* 207 | * Takes the given position and transforms it to the position it should 208 | * have when warped from one output to the other. 209 | */ 210 | position_t pointer_transform_position(position_t pointer, Output *from, Output *to, direction_t direction) { 211 | switch (config.warp_mode) { 212 | case WM_CLOSEST: 213 | return pointer_transform_position_closest(pointer, from, to, direction); 214 | case WM_RELATIVE: 215 | return pointer_transform_position_relative(pointer, from, to, direction); 216 | default: 217 | bail("Unhandled warp mode, bailing out."); 218 | // never reached 219 | return (position_t) { .x = 0, .y = 0 }; 220 | } 221 | } 222 | 223 | /* 224 | * Map the pointer to the correct position if it has to be cycled to the far other 225 | * end of all outputs. 226 | */ 227 | position_t pointer_transform_cycled_position(position_t pointer, Output *to, direction_t direction) { 228 | position_t coordinates = pointer; 229 | switch (direction) { 230 | case D_TOP: 231 | coordinates.y = to->rect.y + to->rect.height - 1; 232 | break; 233 | case D_BOTTOM: 234 | coordinates.y = to->rect.y; 235 | break; 236 | case D_LEFT: 237 | coordinates.x = to->rect.x + to->rect.width - 1; 238 | break; 239 | case D_RIGHT: 240 | coordinates.x = to->rect.x; 241 | break; 242 | default: 243 | ELOG("Unknown direction %d.", direction); 244 | break; 245 | } 246 | 247 | return coordinates; 248 | } 249 | -------------------------------------------------------------------------------- /src/randr.c: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #include "all.h" 3 | 4 | /* List of RandR outputs. */ 5 | struct outputs_head outputs = TAILQ_HEAD_INITIALIZER(outputs); 6 | 7 | /* 8 | * Called by randr_query_outputs to insert / update a particular output in our list. 9 | * 10 | */ 11 | static void randr_handle_output(xcb_randr_output_t id, xcb_randr_get_output_info_reply_t *output, 12 | xcb_timestamp_t timestamp) { 13 | DLOG("Handling output %d", id); 14 | if (output->crtc == XCB_NONE || output->connection == XCB_RANDR_CONNECTION_DISCONNECTED) { 15 | DLOG("Output %d seems to be disabled / disconnected, skipping it.", id); 16 | return; 17 | } 18 | 19 | xcb_randr_get_crtc_info_reply_t *crtc = xcb_randr_get_crtc_info_reply(connection, 20 | xcb_randr_get_crtc_info(connection, output->crtc, timestamp), NULL); 21 | if (crtc == NULL) { 22 | ELOG("Could not receive CRTC information for output %d, skipping it.", id); 23 | return; 24 | } 25 | 26 | Output *new = calloc(sizeof(Output), 1); 27 | if (new == NULL) { 28 | ELOG("Could not alloc space for output %d, skipping it.", id); 29 | return; 30 | } 31 | 32 | new->id = id; 33 | new->rect = (Rect) { 34 | .x = crtc->x, 35 | .y = crtc->y, 36 | .width = crtc->width, 37 | .height = crtc->height 38 | }; 39 | TAILQ_INSERT_TAIL(&outputs, new, outputs); 40 | 41 | DLOG("Added output %d to list of outputs.", id); 42 | FREE(crtc); 43 | } 44 | 45 | /** 46 | * Query RandR outputs. 47 | * 48 | */ 49 | void randr_query_outputs(void) { 50 | if (config.fake_outputs != NULL) { 51 | DLOG("Skipping querying RandR outputs because fake outputs are being used."); 52 | return; 53 | } 54 | 55 | /* First, we make sure the list is empty since this might be called multiple 56 | * times when resolutions change and the like. */ 57 | Output *current = TAILQ_FIRST(&outputs); 58 | while (!TAILQ_EMPTY(&outputs)) { 59 | TAILQ_REMOVE(&outputs, current, outputs); 60 | FREE(current); 61 | current = TAILQ_FIRST(&outputs); 62 | } 63 | 64 | DLOG("Querying RandR outputs..."); 65 | xcb_randr_get_screen_resources_current_reply_t *reply = xcb_randr_get_screen_resources_current_reply( 66 | connection, xcb_randr_get_screen_resources_current(connection, root), NULL); 67 | if (reply == NULL) 68 | bail("Could not receive RandR outputs, bailing out."); 69 | 70 | /* This allows us to ensure that we get consistent information from the server. */ 71 | xcb_timestamp_t timestamp = reply->config_timestamp; 72 | 73 | int len = xcb_randr_get_screen_resources_current_outputs_length(reply); 74 | xcb_randr_output_t *randr_outputs = xcb_randr_get_screen_resources_current_outputs(reply); 75 | for (int i = 0; i < len; i++) { 76 | xcb_randr_get_output_info_reply_t *output = xcb_randr_get_output_info_reply( 77 | connection, xcb_randr_get_output_info(connection, randr_outputs[i], timestamp), NULL); 78 | if (output == NULL) { 79 | DLOG("No output found for id = %d, skipping it.", randr_outputs[i]); 80 | continue; 81 | } 82 | 83 | randr_handle_output(randr_outputs[i], output, timestamp); 84 | FREE(output); 85 | } 86 | 87 | DLOG("Dumping outputs:"); 88 | Output *output; 89 | TAILQ_FOREACH(output, &outputs, outputs) { 90 | DLOG("Output %d: %d / %d / %d / %d.", output->id, output->rect.x, output->rect.y, 91 | output->rect.width, output->rect.height); 92 | } 93 | 94 | FREE(reply); 95 | } 96 | 97 | /* 98 | * Returns the output that contains this position. 99 | * Returns NULL if the position is not on any output. 100 | */ 101 | Output *randr_get_output_containing(position_t pointer) { 102 | Output *current; 103 | TAILQ_FOREACH(current, &outputs, outputs) { 104 | if (pointer.x >= current->rect.x && pointer.x < current->rect.x + current->rect.width && 105 | pointer.y >= current->rect.y && pointer.y < current->rect.y + current->rect.height) { 106 | 107 | TLOG("Found output %d containing position %d / %d", current->id, 108 | pointer.x, pointer.y); 109 | return current; 110 | } 111 | } 112 | 113 | return NULL; 114 | } 115 | 116 | /* 117 | * Returns true if and only if 118 | * - the second output lies (fully) in the given direction relative to the first output and 119 | * - both outputs are touching (no gap in between). 120 | */ 121 | static bool randr_neighbors_in_direction(Output *first_output, Output *second_output, direction_t direction) { 122 | if (first_output == NULL || second_output == NULL) { 123 | ELOG("One of outputs %p / %p is NULL, stopping here.", first_output, second_output); 124 | return false; 125 | } 126 | 127 | Rect *first = &(first_output->rect); 128 | Rect *second = &(second_output->rect); 129 | 130 | switch (direction) { 131 | case D_TOP: 132 | return second->y + second->height == first->y; 133 | case D_LEFT: 134 | return second->x + second->width == first->x; 135 | case D_BOTTOM: 136 | return randr_neighbors_in_direction(second_output, first_output, D_TOP); 137 | case D_RIGHT: 138 | return randr_neighbors_in_direction(second_output, first_output, D_LEFT); 139 | default: 140 | ELOG("Received unknown value %d and don't know what to do with it.", direction); 141 | return false; 142 | } 143 | } 144 | 145 | /* 146 | * Returns either first or second, depending on which one is closer to the given pointer. 147 | * If either one is NULL, the other one is used. 148 | */ 149 | static Output *randr_get_output_closer_to(position_t pointer, direction_t direction, 150 | Output *first, Output *second) { 151 | if (first == NULL || second == NULL) 152 | return first == NULL ? second : first; 153 | 154 | int32_t d_first = 0, 155 | d_second = 0; 156 | switch (direction) { 157 | case D_TOP: 158 | case D_BOTTOM: 159 | d_first = MIN( 160 | ABS((int32_t) pointer.x - (int32_t) first->rect.x), 161 | ABS((int32_t) pointer.x - (int32_t) (first->rect.x + first->rect.width - 1)) 162 | ); 163 | 164 | d_second = MIN( 165 | ABS((int32_t) pointer.x - (int32_t) second->rect.x), 166 | ABS((int32_t) pointer.x - (int32_t) (second->rect.x + second->rect.width - 1)) 167 | ); 168 | 169 | break; 170 | case D_LEFT: 171 | case D_RIGHT: 172 | d_first = MIN( 173 | ABS((int32_t) pointer.y - (int32_t) first->rect.y), 174 | ABS((int32_t) pointer.y - (int32_t) (first->rect.y + first->rect.height)) 175 | ); 176 | 177 | d_second = MIN( 178 | ABS((int32_t) pointer.y - (int32_t) second->rect.y), 179 | ABS((int32_t) pointer.y - (int32_t) (second->rect.y + second->rect.height)) 180 | ); 181 | 182 | break; 183 | default: 184 | ELOG("Unknown direction %d.", direction); 185 | return NULL; 186 | } 187 | 188 | DLOG("Metric of two outputs: first (%p) = %d, second (%p) = %d.", first, d_first, second, d_second); 189 | return d_first < d_second ? first : second; 190 | } 191 | 192 | /* 193 | * Returns the next output in the given direction relative to the specified 194 | * output. Returns NULL if no such output exists. 195 | * The given pointer must lie within the given output. 196 | */ 197 | Output *randr_next_output_in_direction(Output *from, position_t pointer, direction_t direction) { 198 | Output *best = NULL; 199 | 200 | Output *output; 201 | TAILQ_FOREACH(output, &outputs, outputs) { 202 | if (!randr_neighbors_in_direction(from, output, direction)) 203 | continue; 204 | 205 | /* Determine whether this output is better than the one we found 206 | * already (or, if we have none yet, use it). */ 207 | best = randr_get_output_closer_to(pointer, direction, best, output); 208 | } 209 | 210 | return best; 211 | } 212 | 213 | /* 214 | * Returns the next output in the given direction assuming the outputs 215 | * to form a torus shape, i.e., it will actually look on the far opposite side 216 | * of the given direction. 217 | */ 218 | Output *randr_cycle_output_in_direction(position_t pointer, direction_t direction) { 219 | Output *best = NULL; 220 | 221 | Output *output; 222 | TAILQ_FOREACH(output, &outputs, outputs) { 223 | switch (direction) { 224 | case D_TOP: 225 | case D_BOTTOM: 226 | if (output->rect.x + output->rect.width <= pointer.x || output->rect.x > pointer.x) 227 | continue; 228 | 229 | if (best == NULL || 230 | (direction == D_TOP && output->rect.y + output->rect.height > best->rect.y + best->rect.height) || 231 | (direction == D_BOTTOM && output->rect.y < best->rect.y)) { 232 | best = output; 233 | } 234 | break; 235 | case D_LEFT: 236 | case D_RIGHT: 237 | if (output->rect.y + output->rect.height <= pointer.y || output->rect.y > pointer.y) 238 | continue; 239 | 240 | if (best == NULL || 241 | (direction == D_LEFT && output->rect.x + output->rect.width > best->rect.x + best->rect.width) || 242 | (direction == D_RIGHT && output->rect.x < best->rect.x)) { 243 | best = output; 244 | } 245 | break; 246 | default: 247 | ELOG("Unknown direction %d.", direction); 248 | return NULL; 249 | } 250 | } 251 | 252 | return best; 253 | } 254 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #include "all.h" 3 | 4 | /* 5 | * Closes the X connection and errors out with the given message. 6 | */ 7 | void bail(char *message) { 8 | if (connection != NULL) 9 | xcb_disconnect(connection); 10 | ELOG("Received error: %s", message); 11 | errx(EXIT_FAILURE, "%s", message); 12 | } 13 | 14 | /* 15 | * Checks the cookie for errors and returns the result. 16 | */ 17 | bool xcb_return_request_check(xcb_void_cookie_t cookie) { 18 | xcb_generic_error_t *error = xcb_request_check(connection, cookie); 19 | return error == NULL; 20 | } 21 | 22 | /* 23 | * Checks the cookie for errors and bails out if one was returned. 24 | */ 25 | void xcb_request_check_or_bail(xcb_void_cookie_t cookie, char *message) { 26 | if (!xcb_return_request_check(cookie)) 27 | bail(message); 28 | } 29 | -------------------------------------------------------------------------------- /src/xedgewarp.c: -------------------------------------------------------------------------------- 1 | // vim:ts=4:sw=4:expandtab 2 | #include "all.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #ifndef __VERSION 9 | #define __VERSION "unknown" 10 | #endif 11 | 12 | typedef void (*callback)(void); 13 | 14 | /* Forward declarations */ 15 | static void run(void); 16 | static void safe_fork(callback child_callback); 17 | static void print_usage(char *argv[]); 18 | 19 | Display *display; 20 | xcb_connection_t *connection; 21 | xcb_window_t root; 22 | 23 | Config config = { 24 | .fake_outputs = NULL, 25 | .warp_mode = WM_CLOSEST, 26 | .torus_mode = TM_NONE, 27 | .log_level = L_ERROR, 28 | .fork_mode = false 29 | }; 30 | 31 | int main(int argc, char *argv[]) { 32 | atexit(on_xedgewarp_exit); 33 | parse_arguments(argc, argv); 34 | 35 | if (config.fork_mode) { 36 | safe_fork(run); 37 | } else { 38 | run(); 39 | } 40 | } 41 | 42 | static void run(void) { 43 | initialize_x11(); 44 | initialize_xedgewarp(); 45 | 46 | extensions_init(); 47 | randr_query_outputs(); 48 | event_initialize_tree(); 49 | 50 | event_enter_loop(); 51 | 52 | exit(EXIT_SUCCESS); 53 | } 54 | 55 | /* 56 | * Initialize the X server connection. 57 | */ 58 | void initialize_x11(void) { 59 | DLOG("Establishing a connection to the X server..."); 60 | display = XOpenDisplay(NULL); 61 | if (display == NULL) { 62 | bail("Could not connect to X server, bailing out."); 63 | } 64 | 65 | connection = XGetXCBConnection(display); 66 | XSetEventQueueOwner(display, XCBOwnsEventQueue); 67 | 68 | xcb_screen_t *screen = xcb_aux_get_screen(connection, DefaultScreen(display)); 69 | root = screen->root; 70 | DLOG("X server connection established. Let's rock!"); 71 | } 72 | 73 | /* 74 | * Initialize xedgewarp specific things from the parsed 75 | * config. 76 | */ 77 | void initialize_xedgewarp(void) { 78 | if (config.fake_outputs != NULL) { 79 | fake_outputs_create_outputs(config.fake_outputs); 80 | fake_outputs_visualize(); 81 | } 82 | } 83 | 84 | /* 85 | * Called when xedgewarp terminates. 86 | */ 87 | void on_xedgewarp_exit(void) { 88 | if (connection != NULL) 89 | xcb_disconnect(connection); 90 | 91 | FREE(config.fake_outputs); 92 | } 93 | 94 | static void print_usage(char *argv[]) { 95 | fprintf(stderr, "Usage: %s [-b] [-m closest|relative] [-t v|h|both] [-l error|debug|trace] [-v] [-h]", argv[0]); 96 | fprintf(stderr, "\n"); 97 | fprintf(stderr, "\t-h display the usage and exit\n"); 98 | fprintf(stderr, "\t-v display the version and exit\n"); 99 | fprintf(stderr, "\t-b run in background mode, i.e. fork on startup\n"); 100 | fprintf(stderr, "\t-m closest|relative\n" 101 | "\t\tSpecifies how the mouse pointer should be warped.\n"); 102 | fprintf(stderr, "\t-t [none|v|vertical|h|horizontal|both]\n" 103 | "\t\tConnect the far edges of all outputs as if they were to form a torus shape.\n"); 104 | fprintf(stderr, "\t-l error|debug|trace\n" 105 | "\t\tSpecify the log level.\n"); 106 | fprintf(stderr, "\n"); 107 | exit(EXIT_FAILURE); 108 | } 109 | 110 | /* 111 | * Parse command-line arguments. 112 | */ 113 | void parse_arguments(int argc, char *argv[]) { 114 | int c; 115 | while ((c = getopt(argc, argv, ":m:t:l:o:bvh")) != -1) { 116 | switch (c) { 117 | case 'm': 118 | if (strcasecmp(optarg, "closest") == 0) 119 | config.warp_mode = WM_CLOSEST; 120 | else if (strcasecmp(optarg, "relative") == 0) 121 | config.warp_mode = WM_RELATIVE; 122 | else 123 | bail("Unknown warp mode, bailing out."); 124 | 125 | break; 126 | case 't': 127 | if (strcasecmp(optarg, "none") == 0) 128 | config.torus_mode = TM_NONE; 129 | else if (strcasecmp(optarg, "v") == 0 || strcasecmp(optarg, "vertical") == 0) 130 | config.torus_mode = TM_VERTICAL; 131 | else if (strcasecmp(optarg, "h") == 0 || strcasecmp(optarg, "horizontal") == 0) 132 | config.torus_mode = TM_HORIZONTAL; 133 | else if (strcasecmp(optarg, "both") == 0 || strcasecmp(optarg, "all") == 0) 134 | config.torus_mode = TM_VERTICAL | TM_HORIZONTAL; 135 | else 136 | bail("Unknown torus mode, bailing out."); 137 | 138 | break; 139 | case 'l': 140 | if (strcasecmp(optarg, "error") == 0) 141 | config.log_level = L_ERROR; 142 | else if (strcasecmp(optarg, "debug") == 0) 143 | config.log_level = L_DEBUG; 144 | else if (strcasecmp(optarg, "trace") == 0) 145 | config.log_level = L_TRACE; 146 | else 147 | bail("Unknown log level, bailing out."); 148 | break; 149 | case 'o': 150 | config.fake_outputs = strdup(optarg); 151 | break; 152 | case 'b': 153 | config.fork_mode = true; 154 | break; 155 | case 'v': 156 | fprintf(stderr, "%s version %s\n", argv[0], __VERSION); 157 | exit(EXIT_SUCCESS); 158 | break; 159 | case 'h': 160 | print_usage(argv); 161 | break; 162 | case ':': 163 | switch (optopt) { 164 | case 't': 165 | /* For torus mode without a value, we use "both". */ 166 | config.torus_mode = TM_VERTICAL | TM_HORIZONTAL; 167 | break; 168 | default: 169 | print_usage(argv); 170 | break; 171 | } 172 | break; 173 | default: 174 | print_usage(argv); 175 | break; 176 | } 177 | } 178 | } 179 | 180 | /* Double fork to prevent zombie processes */ 181 | static void safe_fork(callback child_callback) { 182 | pid_t pid = fork(); 183 | if (!pid) { 184 | if (!fork()) { 185 | /* this is the child that keeps going */ 186 | (*child_callback)(); 187 | } else { 188 | /* the first child process exits */ 189 | exit(EXIT_SUCCESS); 190 | } 191 | } else { 192 | /* this is the original process */ 193 | /* wait for the first child to exit which it will immediately */ 194 | waitpid(pid, NULL, 0); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /test/run.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use strict; 5 | use warnings; 6 | use utf8; 7 | use v5.10; 8 | use IPC::System::Simple qw(capture $EXITVAL EXIT_ANY); 9 | 10 | my $pid; 11 | 12 | my @tests = @ARGV; 13 | @tests = if @tests == 0; 14 | 15 | $ENV{XEWDISPLAY} //= ":1"; 16 | 17 | sub wait_for_x { 18 | my $display = substr $ENV{XEWDISPLAY}, 1; 19 | while (1) { 20 | last if -S "/tmp/.X11-unix/X$display"; 21 | } 22 | } 23 | 24 | sub start_x_server { 25 | return if $ENV{XEWDISPLAY} eq $ENV{DISPLAY}; 26 | 27 | $pid = fork; 28 | if ($pid == 0) { 29 | exec "Xephyr", $ENV{XEWDISPLAY}, "-screen", "800x600", "-nolisten", "tcp", "+extension", "XINERAMA"; 30 | exit 1; 31 | } 32 | 33 | wait_for_x; 34 | } 35 | 36 | sub stop_x_server { 37 | if (defined $pid) { 38 | kill(15, $pid); 39 | waitpid($pid, 0); 40 | } 41 | } 42 | 43 | my $failures = 0; 44 | for my $test (@tests) { 45 | start_x_server; 46 | 47 | print "\nRunning $test...\n"; 48 | my @lines = capture(EXIT_ANY, "/bin/sh -c $test"); 49 | 50 | stop_x_server; 51 | 52 | if ($EXITVAL != 0) { 53 | $failures++; 54 | next; 55 | } 56 | 57 | for (@lines) { 58 | print "$_"; 59 | 60 | next unless /^not ok/; 61 | $failures++; 62 | } 63 | } 64 | 65 | if ($failures == 0) { 66 | print "\n\nAll tests successful.\n"; 67 | exit 0; 68 | } else { 69 | print "\n\nFailed tests: $failures\n"; 70 | exit 1; 71 | } 72 | -------------------------------------------------------------------------------- /test/t/33-torus.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # Test warping on the far left / right / top / bottom output cycles. 11 | ################################################################################################### 12 | 13 | # Layout: 14 | # ####### 15 | # 16 | # +---+ 17 | # | 4 | 18 | # +---+---+---+ 19 | # | 2 | 1 | 3 | 20 | # +---+---+---+ 21 | # | 5 | 22 | # +---+ 23 | # 24 | foreach my $torus_mode (('none', 'vertical', 'horizontal', 'both')) { 25 | run_xedgewarp(torus => $torus_mode, outputs => [ 26 | '200x200+200+200', 27 | '200x200+0+200', 28 | '200x200+400+200', 29 | '200x200+200+0', 30 | '200x200+200+400' 31 | ]); 32 | 33 | warp_pointer(300, 300); 34 | 35 | # A: Far left 36 | warp_pointer(0, 300); 37 | $pointer = get_pointer; 38 | if ($torus_mode eq 'horizontal' || $torus_mode eq 'both') { 39 | is($pointer->{x}, 599, 'pointer is warped to the right'); 40 | is($pointer->{y}, 300, 'non-warp direction position does not mutate'); 41 | } else { 42 | is($pointer->{x}, 0, 'nothing happened'); 43 | is($pointer->{y}, 300, 'nothing happened'); 44 | } 45 | 46 | warp_pointer(300, 300); 47 | 48 | # B: Far right 49 | warp_pointer(599, 300); 50 | $pointer = get_pointer; 51 | if ($torus_mode eq 'horizontal' || $torus_mode eq 'both') { 52 | is($pointer->{x}, 0, 'pointer is warped to the left'); 53 | is($pointer->{y}, 300, 'non-warp direction position does not mutate'); 54 | } else { 55 | is($pointer->{x}, 599, 'nothing happened'); 56 | is($pointer->{y}, 300, 'nothing happened'); 57 | } 58 | 59 | warp_pointer(300, 300); 60 | 61 | # C: Far top 62 | warp_pointer(300, 0); 63 | $pointer = get_pointer; 64 | if ($torus_mode eq 'vertical' || $torus_mode eq 'both') { 65 | is($pointer->{x}, 300, 'non-warp direction position does not mutate'); 66 | is($pointer->{y}, 599, 'pointer is warped to the bottom'); 67 | } else { 68 | is($pointer->{x}, 300, 'nothing happened'); 69 | is($pointer->{y}, 0, 'nothing happened'); 70 | } 71 | 72 | warp_pointer(300, 300); 73 | 74 | # D: Far bottom 75 | warp_pointer(300, 599); 76 | $pointer = get_pointer; 77 | if ($torus_mode eq 'vertical' || $torus_mode eq 'both') { 78 | is($pointer->{x}, 300, 'non-warp direction position does not mutate'); 79 | is($pointer->{y}, 0, 'pointer is warped to the top'); 80 | } else { 81 | is($pointer->{x}, 300, 'nothing happened'); 82 | is($pointer->{y}, 599, 'nothing happened'); 83 | } 84 | 85 | exit_xedgewarp; 86 | } 87 | 88 | ################################################################################################### 89 | # Test torus mode on a single screen 90 | ################################################################################################### 91 | 92 | run_xedgewarp(torus => 'both', outputs => ['200x200+0+0']); 93 | 94 | warp_pointer(100, 100); 95 | 96 | warp_pointer(0, 100); 97 | $pointer = get_pointer; 98 | is($pointer->{x}, 199, 'torus mode works on single screen'); 99 | is($pointer->{y}, 100, 'non-warp direction does not mutate'); 100 | 101 | warp_pointer(100, 100); 102 | 103 | warp_pointer(100, 0); 104 | $pointer = get_pointer; 105 | is($pointer->{x}, 100, 'non-warp direction does not mutate'); 106 | is($pointer->{y}, 199, 'torus mode works on single screen'); 107 | 108 | exit_xedgewarp; 109 | 110 | ################################################################################################### 111 | 112 | done_testing; 113 | -------------------------------------------------------------------------------- /test/t/4-warp-mode-relative.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # 7: Test warping left / right with two outputs with relative warp mode. 11 | ################################################################################################### 12 | 13 | # Layout: 14 | # ####### 15 | # 16 | # +---+ 17 | # | 1 +---+ 18 | # +---+ 2 | 19 | # +---+ 20 | # 21 | run_xedgewarp(mode => 'relative', outputs => [ 22 | '200x200+0+0', 23 | '200x200+200+100' 24 | ]); 25 | 26 | # A: Left edge 27 | warp_pointer(200, 250); 28 | $pointer = get_pointer; 29 | is($pointer->{x}, 199, 'pointer is warped to the neighboring output (left)'); 30 | is($pointer->{y}, 150, 'pointer is warped to the correct relative position (y)'); 31 | 32 | # reset pointer to become eligible for warping again 33 | warp_pointer(50, 50); 34 | 35 | # B: Right edge 36 | warp_pointer(199, 50); 37 | $pointer = get_pointer; 38 | is($pointer->{x}, 200, 'pointer is warped to the neighboring output (right)'); 39 | is($pointer->{y}, 150, 'pointer is warped to the correct relative position (y)'); 40 | 41 | exit_xedgewarp; 42 | 43 | ################################################################################################### 44 | # 7: Test warping up / down with two outputs with relative warp mode. 45 | ################################################################################################### 46 | 47 | # Layout: 48 | # ####### 49 | # 50 | # +---+ 51 | # | 1 | 52 | # +-+-+-+ 53 | # | 2 | 54 | # +---+ 55 | # 56 | run_xedgewarp(mode => 'relative', outputs => [ 57 | '200x200+0+0', 58 | '200x200+100+200' 59 | ]); 60 | 61 | # A: Top edge 62 | warp_pointer(250, 200); 63 | $pointer = get_pointer; 64 | is($pointer->{x}, 150, 'pointer is warped to the correct relative position (x)'); 65 | is($pointer->{y}, 199, 'pointer is warped to the neighboring output (top)'); 66 | 67 | # reset pointer to become eligible for warping again 68 | warp_pointer(50, 50); 69 | 70 | # B: Bottom edge 71 | warp_pointer(50, 199); 72 | $pointer = get_pointer; 73 | is($pointer->{x}, 150, 'pointer is warped to the correct relative position (x)'); 74 | is($pointer->{y}, 200, 'pointer is warped to the neighboring output (bottom)'); 75 | 76 | exit_xedgewarp; 77 | 78 | ################################################################################################### 79 | # 7: Test relative warping is actually relative on different-sized outputs. 80 | ################################################################################################### 81 | 82 | # Layout: 83 | # ####### 84 | # 85 | # +---+---+ 86 | # | 1 | | 87 | # +---+ 2 | 88 | # | | 89 | # +---+ 90 | # 91 | run_xedgewarp(mode => 'relative', outputs => [ 92 | '200x200+0+0', 93 | '200x400+200+0' 94 | ]); 95 | 96 | warp_pointer(200, 300); 97 | $pointer = get_pointer; 98 | is($pointer->{x}, 199, 'pointer is warped to the neighboring output'); 99 | is($pointer->{y}, 150, 'pointer is warped to the correct relative position (y)'); 100 | 101 | ################################################################################################### 102 | 103 | done_testing; 104 | -------------------------------------------------------------------------------- /test/t/5-multiple-directions.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # ISSUE #5 11 | # Check that touching the corner of an output considers both possible directions. 12 | ################################################################################################### 13 | 14 | # Layout: 15 | # ####### 16 | # 17 | # +---+ 18 | # | 1 +---+ 19 | # +---+ 2 | 20 | # +---+ 21 | # 22 | run_xedgewarp(outputs => [ 23 | '200x200+0+0', 24 | '200x200+200+100' 25 | ]); 26 | 27 | warp_pointer(199, 0); 28 | $pointer = get_pointer; 29 | is($pointer->{x}, 200, 'pointer is warped to the output on the right despite also touching the top border'); 30 | is($pointer->{y}, 100, 'pointer is warped to the top of the new output'); 31 | 32 | exit_xedgewarp; 33 | 34 | ################################################################################################### 35 | 36 | done_testing; 37 | -------------------------------------------------------------------------------- /test/t/6-prevent-looping.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # ISSUE #6 11 | # Check that we don't "loop", i.e., if we warped the pointer, that warp should not trigger another 12 | # warp. 13 | ################################################################################################### 14 | 15 | # Layout: 16 | # ####### 17 | # 18 | # +---+ 19 | # +---+ | 3 | 20 | # | 1 +-+-+-+ 21 | # +---+ 2 | 22 | # +---+ 23 | # 24 | run_xedgewarp(outputs => [ 25 | '200x200+0+100', 26 | '200x200+200+200', 27 | '200x200+300+0' 28 | ]); 29 | 30 | warp_pointer(199, 150); 31 | $pointer = get_pointer; 32 | is($pointer->{x}, 200, 'pointer is warped to the output on the right and no further'); 33 | is($pointer->{y}, 200, 'pointer is warped to the top of the new output'); 34 | 35 | exit_xedgewarp; 36 | 37 | ################################################################################################### 38 | 39 | done_testing; 40 | -------------------------------------------------------------------------------- /test/t/7-nearest-output.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # 7: When multiple outputs are available, make sure to select the nearest one. 11 | ################################################################################################### 12 | 13 | # Layout: 14 | # ####### 15 | # 16 | # +---+ 17 | # +---+ 2 | 18 | # | +---+ 19 | # | 1 | 20 | # | +---+ 21 | # +---+ 3 | 22 | # +---+ 23 | # 24 | run_xedgewarp(outputs => [ 25 | '200x400+0+100', 26 | '200x200+200+0', 27 | '200x200+200+400' 28 | ]); 29 | 30 | # A: Above half, warp to output 2 31 | warp_pointer(199, 299); 32 | $pointer = get_pointer; 33 | is($pointer->{x}, 200, 'pointer is warped to next output (x)'); 34 | is($pointer->{y}, 199, 'pointer is warped to output 2 (y)'); 35 | 36 | # reset pointer to become eligible for warping again 37 | warp_pointer(100, 300); 38 | 39 | # B: Below half, warp to output 3 40 | warp_pointer(199, 301); 41 | $pointer = get_pointer; 42 | is($pointer->{x}, 200, 'pointer is warped to next output (x)'); 43 | is($pointer->{y}, 400, 'pointer is warped to output 3 (y)'); 44 | 45 | exit_xedgewarp; 46 | 47 | ################################################################################################### 48 | 49 | done_testing; 50 | -------------------------------------------------------------------------------- /test/t/basic.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # Test warping left / right with two outputs. 11 | ################################################################################################### 12 | 13 | # Layout: 14 | # ####### 15 | # 16 | # +---+ 17 | # | 1 +---+ 18 | # +---+ 2 | 19 | # +---+ 20 | # 21 | run_xedgewarp(outputs => [ 22 | '200x200+0+0', 23 | '200x200+200+100' 24 | ]); 25 | 26 | # A: Left edge 27 | warp_pointer(200, 250); 28 | $pointer = get_pointer; 29 | is($pointer->{x}, 199, 'pointer is warped to the neighboring output (left)'); 30 | is($pointer->{y}, 199, 'pointer is warped to the bottom of the new output'); 31 | 32 | # reset pointer to become eligible for warping again 33 | warp_pointer(50, 50); 34 | 35 | # B: Right edge 36 | warp_pointer(199, 50); 37 | $pointer = get_pointer; 38 | is($pointer->{x}, 200, 'pointer is warped to the neighboring output (right)'); 39 | is($pointer->{y}, 100, 'pointer is warped to the top of the new output'); 40 | 41 | exit_xedgewarp; 42 | 43 | ################################################################################################### 44 | # Test warping up / down with two outputs. 45 | ################################################################################################### 46 | 47 | # Layout: 48 | # ####### 49 | # 50 | # +---+ 51 | # | 1 | 52 | # +-+-+-+ 53 | # | 2 | 54 | # +---+ 55 | # 56 | run_xedgewarp(outputs => [ 57 | '200x200+0+0', 58 | '200x200+100+200' 59 | ]); 60 | 61 | # A: Top edge 62 | warp_pointer(250, 200); 63 | $pointer = get_pointer; 64 | is($pointer->{x}, 199, 'pointer is warped to the neighboring output (top)'); 65 | is($pointer->{y}, 199, 'pointer is warped to the right of the new output'); 66 | 67 | # reset pointer to become eligible for warping again 68 | warp_pointer(50, 50); 69 | 70 | # B: Bottom edge 71 | warp_pointer(50, 199); 72 | $pointer = get_pointer; 73 | is($pointer->{x}, 100, 'pointer is warped to the neighboring output (bottom)'); 74 | is($pointer->{y}, 200, 'pointer is warped to the left of the new output'); 75 | 76 | exit_xedgewarp; 77 | 78 | ################################################################################################### 79 | 80 | done_testing; 81 | -------------------------------------------------------------------------------- /test/t/gaps-and-overlay-are-ignored.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # Checks that outputs which have a gap in between or which are not disjoint are not considered 11 | # for warping. 12 | ################################################################################################### 13 | 14 | # Layout: 15 | # ####### 16 | # 17 | # +-----+ +-----+ 18 | # | | | | 19 | # | 1 | | 2 | 20 | # +- - -+ | | 21 | # + - - + +-----+ 22 | # | 3 | 23 | # | | 24 | # +-----+ 25 | # 26 | run_xedgewarp(outputs => [ 27 | '200x200+0+0', 28 | '200x200+300+0', 29 | '200x200+0+100' 30 | ]); 31 | 32 | # A: Output with a gap is not considered 33 | warp_pointer(199, 50); 34 | $pointer = get_pointer; 35 | is($pointer->{x}, 199, 'pointer is not warped to an output with a gap (x)'); 36 | is($pointer->{y}, 50, 'pointer is not warped to an output with a gap (y)'); 37 | 38 | # B: Output which is not disjoint is not considered 39 | warp_pointer(50, 199); 40 | $pointer = get_pointer; 41 | is($pointer->{x}, 50, 'pointer is not warped to an overlaying output (x)'); 42 | is($pointer->{y}, 199, 'pointer is not warped to an overlaying output (y)'); 43 | 44 | exit_xedgewarp; 45 | 46 | ################################################################################################### 47 | 48 | done_testing; 49 | -------------------------------------------------------------------------------- /test/t/prevent-subsequent-warps.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | 4 | use Test::More; 5 | use xewtest; 6 | 7 | my $pointer; 8 | 9 | ################################################################################################### 10 | # Checks that warping is disabled while touching a border after a successful warp. 11 | # Also check that moving away from the border resets this and we become eligible for warping 12 | # again. 13 | ################################################################################################### 14 | 15 | # Layout: 16 | # ####### 17 | # 18 | # +---+ 19 | # +---+ | 3 | 20 | # | 1 +-+-+-+ 21 | # +---+ 2 | 22 | # +---+ 23 | # 24 | run_xedgewarp(outputs => [ 25 | '200x200+0+100', 26 | '200x200+200+200', 27 | '200x200+300+0' 28 | ]); 29 | 30 | # A: Pointer is initially warped. 31 | warp_pointer(199, 150); 32 | $pointer = get_pointer; 33 | is($pointer->{x}, 200, 'pointer is warped (x)'); 34 | is($pointer->{y}, 200, 'pointer is warped (y)'); 35 | 36 | # B: We are now not eligible for warping and moving along the border does nothing 37 | warp_pointer(205, 200); 38 | $pointer = get_pointer; 39 | is($pointer->{x}, 205, 'pointer is not warped (x)'); 40 | is($pointer->{y}, 200, 'pointer is not warped (y)'); 41 | 42 | # C: We move away from the border, become eligible again and move to the border, 43 | # which will now trigger a warp. 44 | warp_pointer(205, 205); 45 | warp_pointer(205, 200); 46 | $pointer = get_pointer; 47 | is($pointer->{x}, 300, 'pointer is warped (x)'); 48 | is($pointer->{y}, 199, 'pointer is warped (y)'); 49 | 50 | exit_xedgewarp; 51 | 52 | ################################################################################################### 53 | 54 | done_testing; 55 | -------------------------------------------------------------------------------- /test/xewtest.pm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # vim:ts=4:sw=4:expandtab 3 | package xewtest; 4 | 5 | use strict; 6 | use warnings; 7 | use utf8; 8 | use v5.10; 9 | 10 | use X11::XCB::Connection; 11 | 12 | use Exporter 'import'; 13 | our @EXPORT = qw( 14 | run_xedgewarp 15 | exit_xedgewarp 16 | warp_pointer 17 | get_pointer 18 | $x 19 | ); 20 | 21 | my $pid; 22 | our $x = X11::XCB::Connection->new(display => $ENV{XEWDISPLAY}); 23 | if ($x->has_error) { 24 | die "Cannot connect to X on display $ENV{XEWDISPLAY}!\n"; 25 | } 26 | 27 | sub run_xedgewarp { 28 | my %args = @_; 29 | $args{outputs} //= [ '400x200+0+0', '400x200+400+0' ]; 30 | $args{mode} //= 'closest'; 31 | $args{torus} //= 'none'; 32 | 33 | # warp the pointer so we have a deterministic start scenario 34 | $x->root->warp_pointer(0, 0); 35 | 36 | $pid = open XEWOUT, '-|'; 37 | if ($pid == 0) { 38 | $ENV{DISPLAY} = $ENV{XEWDISPLAY}; 39 | 40 | open STDERR, '>&STDOUT'; 41 | # we use stdbuf (coreutils) to disable buffering 42 | exec 'stdbuf', '-o', '0', '-e', '0', '../xedgewarp', '-l', 'trace', '-m', $args{mode}, '-t', $args{torus}, '-o', join(',', @{$args{outputs}}); 43 | exit 1; 44 | } 45 | 46 | while () { 47 | return if /Entering event loop/; 48 | } 49 | } 50 | 51 | sub exit_xedgewarp { 52 | if (defined $pid && $pid != 0) { 53 | kill(15, $pid); 54 | } 55 | } 56 | 57 | sub warp_pointer { 58 | my ($pos_x, $pos_y) = (@_); 59 | $x->root->warp_pointer($pos_x, $pos_y); 60 | } 61 | 62 | sub get_pointer { 63 | my $cookie = $x->root->_conn->query_pointer($x->root->id); 64 | my $reply = $x->root->_conn->query_pointer_reply($cookie->{sequence}); 65 | return { 66 | 'x' => $reply->{root_x}, 67 | 'y' => $reply->{root_y} 68 | }; 69 | } 70 | 71 | END { 72 | exit_xedgewarp; 73 | } 74 | 75 | 1; 76 | --------------------------------------------------------------------------------