summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile3
-rw-r--r--README.md2
-rw-r--r--xseam.c189
4 files changed, 196 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9f7b696
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+xseam
+compile_commands.json
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c6efa75
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,3 @@
+all:
+ cc -O2 -Wall -Wextra -o xseam xseam.c -lX11 -lXfixes -lXrandr -lXi
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0e041fe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# xseam
+utility that maps cursor position between monitors of different resolution height.
diff --git a/xseam.c b/xseam.c
new file mode 100644
index 0000000..b672849
--- /dev/null
+++ b/xseam.c
@@ -0,0 +1,189 @@
+#include <X11/Xlib.h>
+#include <X11/extensions/Xfixes.h>
+#include <X11/extensions/Xrandr.h>
+#include <X11/extensions/XInput2.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define INSET 2
+typedef struct { int x, y, w, h; } RECT;
+
+typedef struct {
+ PointerBarrier barrier;
+ int seam_x;
+ RECT left;
+ RECT right;
+} SEAM;
+
+Display *dpy;
+Window root;
+int xi_opcode;
+int randr_event_base, randr_error_base;
+int xfixes_event_base, xfixes_error_base;
+
+SEAM *seams = 0;
+int nseams = 0;
+int seam_cap = 0;
+
+int clamp( int v, int lo, int hi ) {
+ return v < lo ? lo : v > hi ? hi : v;
+}
+
+int yranges_overlap( RECT *a, RECT *b ) {
+ return a->y < b->y + b->h && b->y < a->y + a->h;
+}
+
+void destroy_seams() {
+ for( int i = 0; i < nseams; i++ ) {
+ if( seams[i].barrier )
+ XFixesDestroyPointerBarrier( dpy, seams[i].barrier );
+ }
+ nseams = 0;
+}
+
+void add_seam( int seam_x, RECT L, RECT R ) {
+ int y_top = L.y < R.y ? L.y : R.y;
+ int y_bot = (L.y + L.h) > (R.y + R.h) ? (L.y + L.h) : (R.y + R.h);
+
+ PointerBarrier b = XFixesCreatePointerBarrier( dpy, root, seam_x, y_top, seam_x, y_bot, 0, 0, 0 );
+ if( !b )
+ return;
+
+ if( nseams == seam_cap ) {
+ seam_cap = seam_cap ? seam_cap * 2 : 8;
+ seams = realloc( seams, seam_cap * sizeof( *seams ) );
+ }
+ seams[nseams].barrier = b;
+ seams[nseams].seam_x = seam_x;
+ seams[nseams].left = L;
+ seams[nseams].right = R;
+ nseams++;
+}
+
+void rebuild() {
+ destroy_seams();
+
+ int n = 0;
+ XRRMonitorInfo *mon = XRRGetMonitors( dpy, root, 1, &n );
+ if( !mon || n < 2 ) {
+ if( mon ) XRRFreeMonitors( mon );
+ XFlush( dpy );
+ fprintf( stderr, "xseam: %d monitor(s), nothing to map\n", n );
+ return;
+ }
+
+ RECT *r = calloc( n, sizeof( *r ) );
+ for( int i = 0; i < n; i++ ) {
+ r[i].x = mon[i].x; r[i].y = mon[i].y;
+ r[i].w = mon[i].width; r[i].h = mon[i].height;
+ }
+
+ for( int i = 0; i < n; i++ ) {
+ for( int j = 0; j < n; j++ ) {
+ if( i == j ) continue;
+ if( r[i].x + r[i].w == r[j].x && yranges_overlap( &r[i], &r[j] ) )
+ add_seam( r[j].x, r[i], r[j] );
+ }
+ }
+
+ free( r );
+ XRRFreeMonitors( mon );
+ XFlush( dpy );
+ fprintf( stderr, "xseam: %d monitors, %d seam(s) mapped\n", n, nseams);
+}
+
+void handle_barrier_hit( XIBarrierEvent *ev ) {
+ SEAM *s = 0;
+ for( int i = 0; i < nseams; i++ ) {
+ if( seams[i].barrier == ev->barrier ) { s = &seams[i]; break; }
+ }
+ if( !s )
+ return;
+
+ double dx = ev->dx;
+ if( dx == 0.0 )
+ return;
+
+ int dest_x, dest_y;
+ if( dx < 0.0 ) {
+ double yr = ev->root_y;
+ double yl = s->left.y + (yr - s->right.y) * (double)s->left.h / (double)s->right.h;
+ dest_x = s->seam_x - INSET;
+ dest_y = clamp( (int)(yl + 0.5), s->left.y, s->left.y + s->left.h - 1 );
+ } else {
+ double yl = ev->root_y;
+ double yr = s->right.y + (yl - s->left.y) * (double)s->right.h / (double)s->left.h;
+ dest_x = s->seam_x + INSET;
+ dest_y = clamp( (int)(yr + 0.5), s->right.y, s->right.y + s->right.h - 1 );
+ }
+
+ XWarpPointer( dpy, None, root, 0, 0, 0, 0, dest_x, dest_y );
+ XIBarrierReleasePointer( dpy, ev->deviceid, ev->barrier, ev->eventid );
+ XFlush( dpy );
+}
+
+int main() {
+ dpy = XOpenDisplay( 0 );
+ if( !dpy ) { fprintf( stderr, "xseam: cannot open display\n" ); return 1; }
+ root = DefaultRootWindow( dpy );
+ int ev, err;
+
+ if( !XFixesQueryExtension( dpy, &xfixes_event_base, &xfixes_error_base ) ) {
+ fprintf( stderr, "xseam: XFixes missing\n" );
+ return 1;
+ }
+ if( !XQueryExtension( dpy, "XInputExtension", &xi_opcode, &ev, &err ) ) {
+ fprintf( stderr, "xseam: XInput2 missing\n" );
+ return 1;
+ }
+ if( !XRRQueryExtension( dpy, &randr_event_base, &randr_error_base ) ) {
+ fprintf( stderr, "xseam: RandR missing\n" );
+ return 1;
+ }
+
+ XRRSelectInput( dpy, root, RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask | RROutputChangeNotifyMask );
+
+ unsigned char m[XIMaskLen(XI_BarrierHit)];
+ memset( m, 0, sizeof(m) );
+ XISetMask( m, XI_BarrierHit );
+ XIEventMask mask;
+ mask.deviceid = XIAllMasterDevices;
+ mask.mask_len = sizeof(m);
+ mask.mask = m;
+ XISelectEvents( dpy, root, &mask, 1 );
+
+ rebuild();
+ XSync( dpy, 0 );
+
+ for( ;; ) {
+ XEvent ev;
+ XNextEvent( dpy, &ev );
+
+ if( ev.type == GenericEvent && ev.xcookie.extension == xi_opcode ) {
+ if( XGetEventData( dpy, &ev.xcookie ) ) {
+ if( ev.xcookie.evtype == XI_BarrierHit )
+ handle_barrier_hit( (XIBarrierEvent * )ev.xcookie.data );
+ XFreeEventData( dpy, &ev.xcookie );
+ }
+ }
+ else if( ev.type == randr_event_base + RRScreenChangeNotify ) {
+ XRRUpdateConfiguration( &ev );
+ while( XPending( dpy ) ) {
+ XEvent pk;
+ XPeekEvent( dpy, &pk );
+ if( pk.type == randr_event_base + RRScreenChangeNotify
+ || pk.type == randr_event_base + RRNotify ) {
+ XNextEvent( dpy, &pk );
+ if( pk.type == randr_event_base + RRScreenChangeNotify )
+ XRRUpdateConfiguration( &pk );
+ } else break;
+ }
+ rebuild();
+ }
+ else if( ev.type == randr_event_base + RRNotify ) {
+ rebuild();
+ }
+ }
+}