git.y1.nz

selk

Keyboard-only rectangle selector for X
download: https://git.y1.nz/archives/selk.tar.gz
README | Files | Log | Refs | LICENSE

commit 9b3c57a247e02d09b23d79cf9a615ed7f8db589a
parent 90a7d76fb6a6a9f614dd8f1213dbf710a7c46b4b
Author: Emma Weaver <emma@waeaves.com>
Date:   Wed, 22 Apr 2026 22:51:10 -0400

unpolished viable screenshot-taker

Diffstat:
M.gitignore1+
MMakefile20+++++++-------------
MREADME14+++++++++-----
Mbmp.h5-----
Aconfig.bmp.h2++
Dconfig.default.h7-------
Aconfig.draw.h23+++++++++++++++++++++++
Mdraw.c497+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mdraw.h91+++++++++++++++++++++++++++++++++++++------------------------------------------
Mmain.c131++++++++++++++++++++++++++++++++++++-------------------------------------------
Atest-bmp.c77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest-draw0
Dtest-draw.c372-------------------------------------------------------------------------------
13 files changed, 606 insertions(+), 634 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,4 +1,5 @@ grabs +draw config.h diff --git a/Makefile b/Makefile @@ -1,27 +1,21 @@ include config.mk -CONFIGS = config.h config.mk +grabs: main.c draw.o bmp.o config.mk + cc main.c draw.o bmp.o ${CFLAGS} ${LDFLAGS} -o grabs -grabs: main.c bmp.o draw.o util.o ${CONFIGS} - cc main.c bmp.o ${CFLAGS} ${LDFLAGS} -o grabs +test-bmp: test-bmp.c bmp.o draw.o config.mk + cc test-bmp.c bmp.o ${CFLAGS} ${LDFLAGS} -o test-bmp -test-draw: test-draw.c draw.o ${CONFIGS} - cc test-draw.c draw.o ${CFLAGS} ${LDFLAGS} -o test-draw - -# TODO reimplement a test program of the bmp module - -bmp.o: bmp.c bmp.h ${CONFIGS} +bmp.o: bmp.c bmp.h config.mk cc -c bmp.c ${CFLAGS} ${LDFLAGS} -o bmp.o -draw.o: draw.c draw.h ${CONFIGS} +draw.o: draw.c draw.h config.draw.h config.mk cc -c draw.c ${CFLAGS} ${LDFLAGS} -o draw.o -config.h: config.default.h - cp config.default.h config.h - clean: rm -f draw.o bmp.o util.o draw grabs +# TODO implement clean and install scripts like this # dist: clean # mkdir -p st-$(VERSION) # cp -R FAQ LEGACY TODO LICENSE Makefile README config.mk\ diff --git a/README b/README @@ -1,12 +1,16 @@ - Grabs is a minimal screenshot program based on X11. -Credits: - - selx by NRK for the selection UI code - - me for cobbling things together. +Default controls: + [HJKL]: move + [Shift + HJKL]: move slower + [ERDF]: only move one corner (gives visual feedback) + [M]: set the selection to the current monitor + [N]: set the selection to the current window + [Shift + Enter]: start screen recording + [Enter]: take screenshot +-------------------------------------------------------------------------- TODO: - - add vim-style keyboard controls and GUI for that - add "install" and "clean" directives like what dwm and st have - figure out a space-efficient way to record video - this? https://github.com/lecram/gifenc/tree/master diff --git a/bmp.h b/bmp.h @@ -1,9 +1,6 @@ /* Originally retrieved from github.com/yunhao-qian/save-bmp */ /* Adapted by Emma Weaver on 26-04-16. */ -#ifndef SAVE_BMP_INCLUDE -#define SAVE_BMP_INCLUDE - #include <stdint.h> @@ -23,5 +20,3 @@ enum write_bmp_status write_bmp( ); const char * get_bmp_status_message(enum write_bmp_status status); - -#endif /* SAVE_BMP_INCLUDE */ diff --git a/config.bmp.h b/config.bmp.h @@ -0,0 +1,2 @@ +#define MAX_IMAGE_WIDTH 1366 +#define MAX_IMAGE_HEIGHT 768 diff --git a/config.default.h b/config.default.h @@ -1,7 +0,0 @@ - -#define MAX_SCREEN_WIDTH 1366 -#define MAX_SCREEN_HEIGHT 768 - -const char * line_color_name = "#ff3838"; -const int line_width = 3; - diff --git a/config.draw.h b/config.draw.h @@ -0,0 +1,23 @@ +const char * line_color_name = "#38ff38"; + +const int border_width = 3; + +const int sleep_ms = 30; +const int movement_speed_slow = 2; +const int movement_speed_fast = 12; + +/* key mapping */ +#define KEY_SHIFT XK_Shift_L +#define KEY_CONTROL XK_Control_L +#define KEY_UP XK_k +#define KEY_DOWN XK_j +#define KEY_LEFT XK_h +#define KEY_RIGHT XK_l +#define KEY_DOWN_RIGHT XK_f +#define KEY_DOWN_LEFT XK_d +#define KEY_UP_RIGHT XK_r +#define KEY_UP_LEFT XK_e +#define KEY_MONITOR XK_m +#define KEY_WINDOW XK_n +#define KEY_TAKE XK_Return +#define KEY_CANCEL XK_Escape diff --git a/draw.c b/draw.c @@ -1,18 +1,15 @@ #include <stdio.h> #include <stdlib.h> +#include <unistd.h> #include <X11/Xlib.h> #include <X11/Xutil.h> +#include <X11/extensions/Xrandr.h> +#include "config.draw.h" #include "draw.h" -unsigned int -get_grab_mask() -{ - return ButtonPressMask | PointerMotionMask; -} - void -draw_init(DrawContext *ctx, X11 *x11) +setup_draw(DrawContext *ctx, X11 *x11) { unsigned long mask = 0; XSetWindowAttributes window_attributes; @@ -24,7 +21,7 @@ draw_init(DrawContext *ctx, X11 *x11) x11->display, DefaultScreen(x11->display) ), - ctx->color_name, + line_color_name, (XColor []) {0}, &color ); @@ -37,17 +34,14 @@ draw_init(DrawContext *ctx, X11 *x11) mask |= CWBackPixel; window_attributes.event_mask = StructureNotifyMask; - /* window_attributes.event_mask |= ExposureMask; // NOTE: causes way too many expose events... */ mask |= CWEventMask; - for (int i = 0; i < ARRLEN(x11->window); ++i) { + for (int i = 0; i < WINDOW_COUNT; i++) { x11->window[i] = XCreateWindow( x11->display, x11->root.window, - x11->root.x, - x11->root.y, - x11->root.w, - x11->root.h, + x11->root.x, x11->root.y, + x11->root.w, x11->root.h, 0, CopyFromParent, InputOutput, @@ -63,160 +57,437 @@ draw_init(DrawContext *ctx, X11 *x11) } void -init(DrawContext * ctx, X11 * x11, Point cur) +setup_keyboard(X11 * x11) { - ctx->last = cur; - ctx->start = cur; - // start accepting motion events as well - XChangeActivePointerGrab( - x11->display, get_grab_mask(), x11->cross, CurrentTime - ); + /* when launched via dwm keybinding, it fails the grab since + * dwm has it grabbed already. listen for FocusChangeMask and + * keep retrying. */ + int keyboard_grab_result; + XSelectInput(x11->display, x11->root.window, FocusChangeMask); + do { + keyboard_grab_result = XGrabKeyboard( + x11->display, x11->root.window, 0, + GrabModeAsync, GrabModeAsync, CurrentTime + ); + XEvent junk; + XNextEvent(x11->display, &junk); + } while (keyboard_grab_result == AlreadyGrabbed); + XSelectInput(x11->display, x11->root.window, 0x0); + + if (keyboard_grab_result != GrabSuccess) + fatal("failed to grab keyboard"); } void -actually_draw(DrawContext * ctx, X11 * x11, Point cur) +setup_display(X11 * x11) { - // TODO wtf does this do. Whyyyy do it this wayyyyyy - int * px = ((int [3]) { cur.x, ctx->start.x, cur.x }) + (ctx->start.x < cur.x); - int * py = ((int [3]) { cur.y, ctx->start.y, cur.y }) + (ctx->start.y < cur.y); + x11->display = XOpenDisplay(NULL); + if (x11->display == NULL) + fatal("failed to open X display"); + + XWindowAttributes attributes; + x11->root.window = DefaultRootWindow(x11->display); + + int get_attributes_ret = XGetWindowAttributes( + x11->display, + x11->root.window, + &attributes + ); + if (get_attributes_ret == 0) + fatal("XGetWindowAttributes() failed"); + + x11->root.x = attributes.x; + x11->root.y = attributes.y; + x11->root.w = attributes.width; + x11->root.h = attributes.height; +} - // HACK: -/+1 to fix up the selection around the edges - int x = px[0] - (px[0] - 1 == x11->root.x); - int y = py[0] - (py[0] - 1 == x11->root.y); - px[1] = px[1] + (px[1] + 1 == x11->root.w); - py[1] = py[1] + (py[1] + 1 == x11->root.h); - int w = DIFF(x, px[1]); - int h = DIFF(y, py[1]); +void +select_window(DrawContext * ctx, X11 * x11, XEvent event) +{ + XWindowAttributes window_attributes; + int b; - ctx->last = cur; - ctx->final = (Rect){ x, y, w, h }; + ctx->target.window = event.xkey.subwindow; + if (ctx->target.window == None) + ctx->target.window = event.xkey.root; { - int b = ctx->border_width; - Rect r[] = { - { x-b, y-b, w+b, b }, /* top */ - { x-b, y, b, h+b }, /* left */ - { x, y+h, w+b, b }, /* bottom */ - { x+w, y-b, b, h+b }, /* right */ - }; - for (int i = 0; i < ARRLEN(r); ++i) { - Rect *rp = r + i; - XMoveResizeWindow( - x11->display, x11->window[i], - rp->x, rp->y, rp->w, rp->h - ); - } + int get_attributes_result = XGetWindowAttributes( + x11->display, + ctx->target.window, + &window_attributes + ); + + if (get_attributes_result == 0) + fatal("XGetWindowAttributes() failed"); } - if (ctx->state == STATE_INIT) { - LOG("[selx]: mapping" "\n"); - for (int i = 0; i < ARRLEN(x11->window); ++i) { - XMapWindow(x11->display, x11->window[i]); + b = window_attributes.border_width; + ctx->start.x = window_attributes.x + b; + ctx->start.y = window_attributes.y + b; + ctx->end.x = ctx->start.x + window_attributes.width; + ctx->end.y = ctx->start.y + window_attributes.height; +} + +void +select_monitor(DrawContext * ctx, X11 * x11) +{ + ctx->target.monitor = -64; + + int nmonitors; + XRRMonitorInfo *monitors = XRRGetMonitors( + x11->display, x11->root.window, True, &nmonitors + ); + + if (monitors == NULL) + fatal("XRRGetMonitors() failed"); + + Point * cur = &ctx->end; + for (SIZE_T i = 0; i < nmonitors; ++i) { + XRRMonitorInfo * m = monitors + i; + if ( + cur->x >= m->x && + cur->y >= m->y && + cur->x < (m->x + m->width) && + cur->y < (m->y + m->height) + ) { + ctx->target.monitor = i; + break; } - ctx->state = STATE_DRAW; } - { - Cursor c = x11->dirs[((cur.x > ctx->start.x) << 1) | (cur.y > ctx->start.y)]; - XChangeActivePointerGrab(x11->display, get_grab_mask(), c, CurrentTime); + if (ctx->target.monitor >= nmonitors) + fatal("no such monitor" "\n"); + + XRRMonitorInfo * m = monitors + ctx->target.monitor; + ctx->start.x = m->x; + ctx->start.y = m->y; + ctx->end.x = ctx->start.x + m->width; + ctx->end.y = ctx->start.y + m->height; +} + +Rect +get_rectangle(DrawContext * ctx, X11 * x11) +{ + int px[2] = { ctx->start.x, ctx->end.x }, + py[2] = { ctx->start.y, ctx->end.y }; + + /* ensure positive orientations */ + if (px[1] < px[0]) { + px[0] = ctx->end.x; + px[1] = ctx->start.x; + } + if (py[1] < py[0]) { + py[0] = ctx->end.y; + py[1] = ctx->start.y; + } + + px[0] = CLAMP(x11->root.x, px[0], x11->root.x+x11->root.w); + px[1] = CLAMP(x11->root.x, px[1], x11->root.x+x11->root.w); + py[0] = CLAMP(x11->root.y, py[0], x11->root.y+x11->root.h); + py[1] = CLAMP(x11->root.y, py[1], x11->root.y+x11->root.h); + + int x = px[0]; + int y = py[0]; + int w = DIFF(px[0], px[1]); + int h = DIFF(py[0], py[1]); + + return (Rect) { x, y, w, h }; +} + +Rect +get_corner(DrawContext * ctx) +{ +} + +int +is_fast(DrawContext * ctx) +{ + return !(ctx->keys_down_mask & KEY_SHIFT_MASK); +} + +void +draw(DrawContext * ctx, X11 * x11) +{ + Rect selection; + int x, y, w, h, b; + Rect * rects; + + /* clamp the control points... */ + ctx->start.x = CLAMP(x11->root.x, ctx->start.x, x11->root.x+x11->root.w); + ctx->start.y = CLAMP(x11->root.y, ctx->start.y, x11->root.y+x11->root.h); + ctx->end.x = CLAMP(x11->root.x, ctx->end.x, x11->root.x+x11->root.w); + ctx->end.y = CLAMP(x11->root.y, ctx->end.y, x11->root.y+x11->root.h); + + selection = get_rectangle(ctx, x11); + x = selection.x; + y = selection.y; + w = selection.w; + h = selection.h; + b = border_width; + rects = (Rect[]) { + { x-b, y-b, w+b, b }, /* top */ + { x-b, y, b, h+b }, /* left */ + { x, y+h, w+b, b }, /* bottom */ + { x+w, y-b, b, h+b }, /* right */ + { x+w/2-b/2,y+h/2-b/2,b,b }, /* center or corner */ + }; + + /* show corner if one is selected */ + if (ctx->keys_down_mask & KEY_DOWN_RIGHT_MASK) + rects[4] = (Rect) { x+w-b,y+h-b,b,b }; + if (ctx->keys_down_mask & KEY_UP_RIGHT_MASK) + rects[4] = (Rect) { x+w-b,y,b,b }; + if (ctx->keys_down_mask & KEY_DOWN_LEFT_MASK) + rects[4] = (Rect) { x,y+h-b,b,b }; + if (ctx->keys_down_mask & KEY_UP_LEFT_MASK) + rects[4] = (Rect) { x,y,b,b }; + + /* make the indicator bigger when fast */ + if (is_fast(ctx)) { + rects[4].x -= b; + rects[4].y -= b; + rects[4].w += 2*b; + rects[4].h += 2*b; + } + + for (int i = 0; i < WINDOW_COUNT; i++) { + Rect * rect = rects + i; + XMoveResizeWindow( + x11->display, x11->window[i], + rect->x, rect->y, + rect->w, rect->h + ); } XFlush(x11->display); } void -clean(DrawContext * ctx, X11 * x11, int event) +init_draw(DrawContext * ctx, X11 * x11) // , Point p) +{ + ctx->start.x = border_width * 2; + ctx->start.y = border_width * 2; + ctx->end.x = ctx->start.x + 400; + ctx->end.y = ctx->start.y + 400; + + draw(ctx, x11); + + for (int i = 0; i < ARRLEN(x11->window); i++) + XMapWindow(x11->display, x11->window[i]); + + XFlush(x11->display); +} + +void +set_up(DrawContext * ctx, X11 * x11) { - LOG("[selx]: DRAW_STATE_CLEAN" "\n"); - ASSERT(ctx->state >= STATE_INIT && ctx->state < STATE_EXIT_LOOP); + setup_display(x11); + setup_keyboard(x11); + setup_draw(ctx, x11); + init_draw(ctx, x11); +} +void +clean_up(DrawContext * ctx, X11 * x11) +{ for (int i = 0; i < ARRLEN(x11->window); ++i) XDestroyWindow(x11->display, x11->window[i]); int destroyed = 0; - int unmapped = (ctx->state == STATE_INIT) ? 4 : 0; - for (XEvent event; !(destroyed == 4 && unmapped == 4); NOP()) { + int unmapped = 0; + for (XEvent event; !(destroyed == WINDOW_COUNT && unmapped == WINDOW_COUNT);) { XNextEvent(x11->display, &event); - for (int i = 0; i < ARRLEN(x11->window); ++i) switch(event.type) { // NOLINT(bugprone-switch*) - case DestroyNotify: - destroyed += (event.xdestroywindow.window == x11->window[i]); - break; - case UnmapNotify: - unmapped += (event.xunmap.window == x11->window[i]); - break; + for (int i = 0; i < ARRLEN(x11->window); i++) { + if(event.type == DestroyNotify) + destroyed += (event.xdestroywindow.window == x11->window[i]); + if(event.type == UnmapNotify) + unmapped += (event.xunmap.window == x11->window[i]); } } + + XUngrabPointer(x11->display, CurrentTime); + XUngrabKeyboard(x11->display, CurrentTime); } void -swap(DrawContext * ctx, X11 * x11, Point * cur) -{ - LOG("[selx]: swap" "\n"); - Point tmp = * cur; - * cur = ctx->start; - ctx->start = tmp; - XWarpPointer( - x11->display, None, x11->root.window, 0, 0, 0, 0, - cur->x, cur->y - ); +handle_key_press(DrawContext * ctx, X11 * x11, XEvent event) +{ + KeySym keysym = XKeycodeToKeysym(x11->display, event.xkey.keycode, 0); + + /* check for keyboard interrupt */ + if ((event.xkey.state & ControlMask) && keysym == XK_c) + fatal("Keyboard Interrupt" "\n"); + + switch (keysym) { + case KEY_MONITOR: + select_monitor(ctx, x11); + draw(ctx, x11); + break; + case KEY_WINDOW: + select_window(ctx, x11, event); + draw(ctx, x11); + break; + case KEY_TAKE: + { + int shift = ctx->keys_down_mask & KEY_SHIFT_MASK; + ctx->state = shift ? STATE_RECORD : STATE_TAKE; + } + break; + case KEY_CANCEL: + fatal("Cancelled" "\n"); + break; + case KEY_SHIFT: ctx->keys_down_mask |= KEY_SHIFT_MASK; break; + case KEY_CONTROL: ctx->keys_down_mask |= KEY_CONTROL_MASK; break; + case KEY_LEFT: ctx->keys_down_mask |= KEY_LEFT_MASK; break; + case KEY_RIGHT: ctx->keys_down_mask |= KEY_RIGHT_MASK; break; + case KEY_UP: ctx->keys_down_mask |= KEY_UP_MASK; break; + case KEY_DOWN: ctx->keys_down_mask |= KEY_DOWN_MASK; break; + case KEY_DOWN_LEFT: ctx->keys_down_mask |= KEY_DOWN_LEFT_MASK; break; + case KEY_DOWN_RIGHT: ctx->keys_down_mask |= KEY_DOWN_RIGHT_MASK; break; + case KEY_UP_LEFT: ctx->keys_down_mask |= KEY_UP_LEFT_MASK; break; + case KEY_UP_RIGHT: ctx->keys_down_mask |= KEY_UP_RIGHT_MASK; break; + } + + /* draw to update visual hinting */ + draw(ctx, x11); } void -handle_wait(DrawContext * ctx, X11 * x11, Point cur, int event) +handle_key_release(DrawContext * ctx, X11 * x11, XEvent event) { - if (event == EVENT_CLICK) { - init(ctx, x11, cur); - ctx->state = STATE_INIT; + KeySym keysym = XKeycodeToKeysym( + x11->display, event.xkey.keycode, 0 + ); + + switch (keysym) { + case KEY_SHIFT: ctx->keys_down_mask &= ~KEY_SHIFT_MASK; break; + case KEY_CONTROL: ctx->keys_down_mask &= ~KEY_CONTROL_MASK; break; + case KEY_LEFT: ctx->keys_down_mask &= ~KEY_LEFT_MASK; break; + case KEY_RIGHT: ctx->keys_down_mask &= ~KEY_RIGHT_MASK; break; + case KEY_DOWN: ctx->keys_down_mask &= ~KEY_DOWN_MASK; break; + case KEY_UP: ctx->keys_down_mask &= ~KEY_UP_MASK; break; + case KEY_DOWN_LEFT: ctx->keys_down_mask &= ~KEY_DOWN_LEFT_MASK; break; + case KEY_DOWN_RIGHT: ctx->keys_down_mask &= ~KEY_DOWN_RIGHT_MASK; break; + case KEY_UP_LEFT: ctx->keys_down_mask &= ~KEY_UP_LEFT_MASK; break; + case KEY_UP_RIGHT: ctx->keys_down_mask &= ~KEY_UP_RIGHT_MASK; break; } - if (event == EVENT_ABORT) - ctx->state = STATE_ABORT; + /* draw to update visual hinting */ + draw(ctx, x11); } - + void -handle_init(DrawContext * ctx, X11 * x11, Point cur, int event) +handle_motion_notify(DrawContext * ctx, X11 * x11, XEvent event, int * queued) { - if (event == EVENT_MOTION) - actually_draw(ctx, x11, cur); + Point p = { event.xmotion.x, event.xmotion.y }; + int ndiscarded = 0; - if (event == EVENT_ABORT || event == EVENT_CLICK) { - clean(ctx, x11, event); - ctx->state = STATE_ABORT; + for ( int n = 0; n > 0 || (n = XPending(x11->display)) > 0; n--) { + XNextEvent(x11->display, &event); + if (event.type == MotionNotify) { + p = (Point) { event.xmotion.x, event.xmotion.y }; + ndiscarded++; + } else { + * queued = TRUE; + LOG("[selx]: queuing event" "\n"); + break; + } } + + if (ndiscarded) + LOG("[selx]: discarded %d stale MotionNotify event" "\n", ndiscarded); + + // ctx->end = p; + // draw(ctx, x11); } void -handle_draw(DrawContext * ctx, X11 * x11, Point cur, int event) +handle_xevent(DrawContext * ctx, X11 * x11, XEvent event, int * queued) { - if (event == EVENT_SWAP) { - swap(ctx, x11, &cur); - actually_draw(ctx, x11, cur); + switch (event.type) { + case KeyPress: + handle_key_press(ctx, x11, event); + break; + case KeyRelease: + handle_key_release(ctx, x11, event); + break; + case MotionNotify: + // handle_motion_notify(ctx, x11, event, queued); + break; + case Expose: + draw(ctx, x11); + break; + case DestroyNotify: + fatal("received DestroyNotify" "\n"); + break; } +} + +void +do_movement(DrawContext * ctx, X11 * x11) +{ + int mask = ctx->keys_down_mask; + int down_right = (mask & KEY_DOWN_RIGHT_MASK) != 0, + up_right = (mask & KEY_UP_RIGHT_MASK) != 0, + down_left = (mask & KEY_DOWN_LEFT_MASK) != 0, + up_left = (mask & KEY_UP_LEFT_MASK) != 0; + int shift = (mask & KEY_SHIFT_MASK) != 0, + controlled = (mask & KEY_CONTROL_MASK) != 0; + int any_corners = down_right || up_right || down_left || up_left; - if (event == EVENT_MOTION) - actually_draw(ctx, x11, cur); + int delta_x = ((mask & KEY_RIGHT_MASK) != 0) - + ((mask & KEY_LEFT_MASK) != 0), + delta_y = ((mask & KEY_DOWN_MASK) != 0) - + ((mask & KEY_UP_MASK) != 0); - if (event == EVENT_CLICK) { - actually_draw(ctx, x11, cur); - clean(ctx, x11, event); - ctx->state = STATE_RECT; - } + int speed = is_fast(ctx) ? movement_speed_fast : movement_speed_slow; + + if (delta_x == 0 && delta_y == 0) + return; - if (event == EVENT_ABORT) { - clean(ctx, x11, event); - ctx->state = STATE_ABORT; + if (!any_corners) { + /* translate the whole selection, */ + ctx->start.x += speed * delta_x; + ctx->start.y += speed * delta_y; + ctx->end.x += speed * delta_x; + ctx->end.y += speed * delta_y; + } else { + /* only move specific corners */ + if (down_right || down_left) ctx->end.y += speed * delta_y; + if (up_right || up_left) ctx->start.y += speed * delta_y; + if (down_right || up_right) ctx->end.x += speed * delta_x; + if (down_left || up_left) ctx->start.x += speed * delta_x; } + + draw(ctx, x11); + ctx->frames_moving++; } void -draw(DrawContext * ctx, X11 * x11, Point cur, int event) +run_draw_loop(DrawContext * ctx, X11 * x11) { - ASSERT(event >= EVENT_CLICK && event <= EVENT_ABORT); + int queued = FALSE, event_set; + XEvent event; + + for (;;) { + if (ctx->state == STATE_TAKE || ctx->state == STATE_RECORD) + break; - if (ctx->state == STATE_WAIT) - handle_wait(ctx, x11, cur, event); - else if (ctx->state == STATE_INIT) - handle_init(ctx, x11, cur, event); - else if (ctx->state == STATE_DRAW) - handle_draw(ctx, x11, cur, event); + event_set = XCheckMaskEvent( + x11->display, + ~0, /* accept all events */ + &event + ); + + if (!event_set) { + int sleep_result = usleep(sleep_ms * 1000); + if (ctx->keys_down_mask != 0) do_movement(ctx, x11); + else ctx->frames_moving = 0; + continue; + } + + handle_xevent(ctx, x11, event, &queued); + } } diff --git a/draw.h b/draw.h @@ -1,14 +1,18 @@ +/* Originally retrieved from https://codeberg.org/NRK/selx */ +/* Adapted by Emma Weaver on 26-04-20. */ + //////////////// macros -#define NOP() ((void)0) -#define DIFF(A, B) ((A) > (B) ? (A) - (B) : (B) - (A)) -#define SIZEOF(...) ((Size)sizeof(__VA_ARGS__)) -#define ARRLEN(...) (SIZEOF(__VA_ARGS__) / SIZEOF(0[__VA_ARGS__])) -#define ARREND(V) ((V) + ARRLEN(V)) -#define ASSERT(X) ((void)0) +#define DIFF(A, B) ((A) > (B) ? (A) - (B) : (B) - (A)) +#define SIZE_T ptrdiff_t +#define SIZEOF(...) ((SIZE_T)sizeof(__VA_ARGS__)) +#define ARRLEN(...) (SIZEOF(__VA_ARGS__) / SIZEOF(0[__VA_ARGS__])) +#define ARREND(V) ((V) + ARRLEN(V)) +#define CLAMP(lo, x, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : x)) #define TRUE 1 #define FALSE 0 +#define WINDOW_COUNT 5 #define LOG(...) printf(__VA_ARGS__) @@ -17,37 +21,25 @@ //////////////// types typedef enum { - DRAW_STATE_INIT = 1 << 0, - DRAW_STATE_DRAW = 1 << 1, - DRAW_STATE_CLEAN = 1 << 2, - DRAW_STATE_FINISH = 1 << 3, - DRAW_STATE_ABORT = 1 << 4, - DRAW_STATE_SWAP = 1 << 5 -} DrawState; - -typedef enum { - FEATURE_WM_BORDER = 1 << 0, - FEATURE_KEYBOARD = 1 << 1, - FEATURE_MAUS = 1 << 2, - FEATURE_ROOT = 1 << 3, - FEATURE_CURSOR = 1 << 4, -} FeatureFlag; - -typedef enum { - STATE_WAIT, - STATE_INIT, - STATE_DRAW, - // terminating states. don't mess with the order of this - STATE_EXIT_LOOP, - STATE_RECT, - STATE_WINDOW, - STATE_MONITOR, - STATE_ABORT, + STATE_NONE, + STATE_TAKE, /* take screenshot */ + STATE_RECORD /* take screen recording */ } State; - -enum { EVENT_CLICK, EVENT_MOTION, EVENT_SWAP, EVENT_ABORT }; - -typedef ptrdiff_t Size; +enum { EVENT_CLICK, EVENT_MOTION, EVENT_SWAP }; +enum { + KEY_UP_MASK = 1 << 0, + KEY_DOWN_MASK = 1 << 1, + KEY_RIGHT_MASK = 1 << 2, + KEY_LEFT_MASK = 1 << 3, + + KEY_UP_RIGHT_MASK = 1 << 4, + KEY_UP_LEFT_MASK = 1 << 5, + KEY_DOWN_RIGHT_MASK = 1 << 6, + KEY_DOWN_LEFT_MASK = 1 << 7, + + KEY_CONTROL_MASK = 1 << 8, + KEY_SHIFT_MASK = 1 << 9 +}; typedef struct { int x, y; } Point; typedef struct { int x, y, w, h; } Rect; @@ -58,22 +50,18 @@ typedef struct { Window window; int x, y, w, h; } root; - - Cursor cross; - Cursor dirs[4]; - - // shape - Window window[4]; + Window window[WINDOW_COUNT]; } X11; -enum { MONITOR_UNDER_CURSOR = -64 }; typedef struct { - Point start, last; - Rect final; + int keys_down_mask; + int frames_moving; + + Point start, end; State state; union { Window window; - int monitor; + int monitor; } target; const char * color_name; const int border_width; @@ -81,6 +69,13 @@ typedef struct { ///////////////////////////////// functions -void draw_init(DrawContext *ctx, X11 *x11); -void draw(DrawContext *ctx, X11 *x11, Point cur, int event); +void set_up(DrawContext * ctx, X11 * x11); +void clean_up(DrawContext * ctx, X11 * x11); +void select_monitor(DrawContext * ctx, X11 * x11); +void select_window(DrawContext * ctx, X11 * x11, XEvent event); +int is_fast(DrawContext * ctx); + +Rect get_rectangle(DrawContext * ctx, X11 * x11); +void draw(DrawContext * ctx, X11 * x11); +void run_draw_loop(DrawContext * ctx, X11 * x11); diff --git a/main.c b/main.c @@ -1,93 +1,82 @@ +#include <limits.h> +#include <stddef.h> +#include <stdint.h> #include <stdio.h> + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + #include <X11/Xlib.h> #include <X11/Xutil.h> -#include <stdlib.h> -#include <limits.h> +#include <X11/keysym.h> -#include "config.h" +#include "draw.h" #include "bmp.h" -#define CANCEL(message) { fprintf(stderr, (message)); exit(1); } +#define MAX_IMAGE_WIDTH 1366 +#define MAX_IMAGE_HEIGHT 768 +const char * usage = "Usage: grabs" "\n"; +uint8_t buffer[MAX_IMAGE_WIDTH * MAX_IMAGE_HEIGHT * 3]; -uint8_t image_buffer[3 * MAX_SCREEN_WIDTH * MAX_SCREEN_HEIGHT]; - -int -stoi(char * arg) -{ - char * endptr; - int ret; - ret = strtol(arg, &endptr, 10); - if (* endptr != '\0' || ret > INT_MAX || ret < INT_MIN) - CANCEL("Couldn't parse arguments." "\n"); - return ret; -} - -int -main(int argc, char * argv[]) +void +write_screenshot_to_buffer(X11 * x11, Rect target, uint8_t * image_buffer) { XImage * ximage; - XWindowAttributes wa; - Display * display; - Window window; - int start_x = 0, start_y = 0, width, height; - - display = XOpenDisplay(NULL); - if (display == NULL) - CANCEL("Couldn't open display." "\n"); - - window = RootWindow(display, DefaultScreen(display)); - - if (argc == 1) { - int get_result = XGetWindowAttributes(display, window, &wa); - if (get_result == 0) - CANCEL("XGetWindowAttributes failed." "\n"); - width = wa.width; - height = wa.height; - } else if (argc == 5) { - start_x = stoi(argv[1]); - start_y = stoi(argv[2]); - width = stoi(argv[3]); - height = stoi(argv[4]); - } else { - CANCEL( - "grabs [x y w h]" "\n" \ - "Minimal screenshot utility." "\n" - ); - } + int x = 0, y = 0; + long pixel; + uint8_t * buffer_index = image_buffer; ximage = XGetImage( - display, window, - start_x, start_y, width, height, + x11->display, x11->root.window, + target.x, target.y, target.w, target.h, AllPlanes, ZPixmap ); - { - int x = 0, y = 0; - long pixel; - uint8_t * buffer_index = image_buffer; - - for (;;) { - pixel = XGetPixel(ximage, x, y); - - *buffer_index++ = (pixel & ximage->red_mask) >> 16; - *buffer_index++ = (pixel & ximage->green_mask) >> 8; - *buffer_index++ = pixel & ximage->blue_mask; - - x++; - if (x >= width) { - x = 0; - y++; - } - if (y >= height) - break; + for (;;) { + pixel = XGetPixel(ximage, x, y); + + *buffer_index++ = (pixel & ximage->red_mask) >> 16; + *buffer_index++ = (pixel & ximage->green_mask) >> 8; + *buffer_index++ = pixel & ximage->blue_mask; + + x++; + if (x >= target.w) { + x = 0; + y++; } + if (y >= target.h) + break; } +} + +int +main(int argc, const char * argv) +{ + X11 x11; + DrawContext ctx = {}; + + set_up(&ctx, &x11); + run_draw_loop(&ctx, &x11); + clean_up(&ctx, &x11); + Rect selection = get_rectangle(&ctx, &x11); + + /* + printf( + "%d %d %d %d" "\n", + selection.x, selection.y, selection.w, selection.h + ); + */ + write_screenshot_to_buffer(&x11, selection, buffer); + + const int status = write_bmp(stdout, buffer, selection.w, selection.h); - const int status = write_bmp(stdout, image_buffer, width, height); if (status != 0) - CANCEL(get_bmp_status_message(status)); + fatal(get_bmp_status_message(status)); + + XCloseDisplay(x11.display); - return 1; + return 0; } diff --git a/test-bmp.c b/test-bmp.c @@ -0,0 +1,77 @@ +#include <stdio.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <stdlib.h> +#include <limits.h> + +#include "config.bmp.h" +#include "bmp.h" + +#define CANCEL(message) { fprintf(stderr, (message)); exit(1); } + + +uint8_t image_buffer[3 * MAX_IMAGE_WIDTH * MAX_IMAGE_HEIGHT]; + +int +main(int argc, char * argv[]) +{ + XImage * ximage; + XWindowAttributes wa; + Display * display; + Window window; + int start_x = 0, start_y = 0, width, height; + + display = XOpenDisplay(NULL); + if (display == NULL) + CANCEL("Couldn't open display." "\n"); + + window = RootWindow(display, DefaultScreen(display)); + + if (argc == 1) { + int get_result = XGetWindowAttributes(display, window, &wa); + if (get_result == 0) + CANCEL("XGetWindowAttributes failed." "\n"); + width = wa.width; + height = wa.height; + } else { + CANCEL( + "grabs [x y w h]" "\n" \ + "Minimal screenshot utility." "\n" + ); + } + + ximage = XGetImage( + display, window, + start_x, start_y, width, height, + AllPlanes, ZPixmap + ); + + { + int x = 0, y = 0; + long pixel; + uint8_t * buffer_index = image_buffer; + + for (;;) { + pixel = XGetPixel(ximage, x, y); + + *buffer_index++ = (pixel & ximage->red_mask) >> 16; + *buffer_index++ = (pixel & ximage->green_mask) >> 8; + *buffer_index++ = pixel & ximage->blue_mask; + + x++; + if (x >= width) { + x = 0; + y++; + } + if (y >= height) + break; + } + } + + const int status = write_bmp(stdout, image_buffer, width, height); + if (status != 0) + CANCEL(get_bmp_status_message(status)); + + return 1; +} + diff --git a/test-draw b/test-draw Binary files differ. diff --git a/test-draw.c b/test-draw.c @@ -1,372 +0,0 @@ -#include <limits.h> -#include <stddef.h> -#include <stdint.h> -#include <stdio.h> - -#include <stdlib.h> -#include <string.h> -#include <unistd.h> - -#include <X11/Xlib.h> -#include <X11/Xutil.h> -#include <X11/keysym.h> -#include <X11/cursorfont.h> -#include <X11/extensions/Xrandr.h> - -#include "config.h" -#include "draw.h" - -const char * usage = "Usage: grabs" "\n"; -const uint32_t features = - FEATURE_KEYBOARD | FEATURE_MAUS | FEATURE_CURSOR | FEATURE_ROOT; - -void -do_window(DrawContext * ctx, Point p, XEvent event) -{ - if (ctx->state != STATE_WAIT) - return; - - if (event.xkey.state & ControlMask) { - ctx->state = STATE_MONITOR; - ctx->last = p; - ctx->target.monitor = MONITOR_UNDER_CURSOR; - return; - } - - ctx->state = STATE_WINDOW; - ctx->target.window = event.xkey.subwindow; - if (ctx->target.window == None) - ctx->target.window = event.xkey.root; -} - -void -handle_key_press(DrawContext * ctx, X11 * x11, XEvent event) -{ - KeySym keysym = XKeycodeToKeysym( - x11->display, event.xkey.keycode, 0 - ); - - Point p = { event.xkey.x, event.xkey.y }; - int control_pressed = event.xkey.state & ControlMask; - int shift_pressed = event.xkey.state & ShiftMask; - int delta = control_pressed ? 1 : (shift_pressed ? 128 : 16); - - switch (keysym) { - case XK_w: - do_window(ctx, p, event); - break; - - case XK_space: - draw(ctx, x11, p, EVENT_CLICK); - break; - - case XK_f: - draw(ctx, x11, p, EVENT_SWAP); - break; - - case XK_q: - case XK_Escape: - draw(ctx, x11, p, EVENT_ABORT); - break; - - case XK_h: - case XK_Left: - p.x -= delta; - break; - - case XK_l: - case XK_Right: - p.x += delta; - break; - - case XK_k: - case XK_Up: - p.y -= delta; - break; - - case XK_j: - case XK_Down: - p.y += delta; - break; - } - - if ( - p.x != event.xkey.x_root || - p.y != event.xkey.y_root - ) - XWarpPointer( - x11->display, None, x11->root.window, - 0, 0, 0, 0, - p.x, p.y - ); -} - -void -handle_button_press(DrawContext * ctx, X11 * x11, XEvent event) -{ - int btn = event.xbutton.button; - Point p = { event.xbutton.x, event.xbutton.y }; - - if (btn == Button1) { - draw(ctx, x11, p, EVENT_CLICK); - return; - } - - if (btn == Button3 && ctx->state == STATE_WAIT) { - // this is kinda bad right...? - int control_pressed = event.xbutton.state & ControlMask; - if (control_pressed) { - ctx->state = STATE_MONITOR; - ctx->last = p; - ctx->target.monitor = MONITOR_UNDER_CURSOR; - } else { - ctx->state = STATE_WINDOW; - ctx->target.window = event.xbutton.subwindow; - if (ctx->target.window == None) - ctx->target.window = event.xbutton.root; - } - return; - } - - if ((btn == Button4 || btn == Button5) && ctx->state == STATE_DRAW) { - draw(ctx, x11, p, EVENT_SWAP); - return; - } - - draw(ctx, x11, p, EVENT_ABORT); -} - -void -handle_motion_notify(DrawContext * ctx, X11 * x11, XEvent event, int * queued) -{ - Point p = { event.xmotion.x, event.xmotion.y }; - int ndiscarded = 0; - - for (int n = 0; n > 0 || (n = XPending(x11->display)) > 0; --n) { - XNextEvent(x11->display, &event); - if (event.type == MotionNotify) { - p = (Point) { event.xmotion.x, event.xmotion.y }; - ndiscarded++; - } else { - * queued = TRUE; - LOG("[selx]: queuing event" "\n"); - break; - } - } - - if (ndiscarded) - LOG("[selx]: discarded %d stale MotionNotify event" "\n", ndiscarded); - - draw(ctx, x11, p, EVENT_MOTION); -} - -int -main() -{ - X11 x11; - DrawContext ctx = { - .color_name = line_color_name, - .border_width = line_width, - }; - - if (FALSE) { /* window mode */ - // ctx.state = STATE_WINDOW; - // ctx.target.window = <WINDOW ID>; - // features = FEATURE_ROOT; - } - - if (FALSE) { /* monitor mode */ - // ctx.state = STATE_MONITOR; - // ctx.target.monitor = <MONITOR ID>; - // features = FEATURE_ROOT; - } - - x11.display = XOpenDisplay(NULL); - if (x11.display == NULL) - fatal("failed to open X display"); - - if (features & FEATURE_ROOT) { - XWindowAttributes attributes; - x11.root.window = DefaultRootWindow(x11.display); - - int get_attributes_ret = XGetWindowAttributes( - x11.display, - x11.root.window, - &attributes - ); - if (get_attributes_ret == 0) - fatal("XGetWindowAttributes() failed"); - - x11.root.x = attributes.x; - x11.root.y = attributes.y; - x11.root.w = attributes.width; - x11.root.h = attributes.height; - } - - if (features & FEATURE_CURSOR) { - x11.dirs[0x0] = XCreateFontCursor(x11.display, XC_ul_angle); - x11.dirs[0x1] = XCreateFontCursor(x11.display, XC_ll_angle); - x11.dirs[0x2] = XCreateFontCursor(x11.display, XC_ur_angle); - x11.dirs[0x3] = XCreateFontCursor(x11.display, XC_lr_angle); - } - - if (features & FEATURE_KEYBOARD) { - /* when launched via dwm keybinding, it fails the grab since - * dwm has it grabbed already. listen for FocusChangeMask and - * keep retrying. */ - int res; - XSelectInput(x11.display, x11.root.window, FocusChangeMask); - do { - res = XGrabKeyboard( - x11.display, x11.root.window, 0, - GrabModeAsync, GrabModeAsync, CurrentTime - ); - XEvent junk; - XNextEvent(x11.display, &junk); - } while (res == AlreadyGrabbed); - XSelectInput(x11.display, x11.root.window, 0x0); - - if (res != GrabSuccess) - fatal("failed to grab keyboard"); - } - - if (features & FEATURE_MAUS) { - x11.cross = XCreateFontCursor(x11.display, XC_cross); - int res = XGrabPointer( - x11.display, x11.root.window, 0, - ButtonPressMask, GrabModeAsync, - GrabModeAsync, x11.root.window, x11.cross, CurrentTime - ); - - if (res != GrabSuccess) - fatal("failed to grab cursor"); - } - - draw_init(&ctx, &x11); - - int queued = FALSE; - for (XEvent event; ctx.state < STATE_EXIT_LOOP; NOP()) { - if (!queued) - XNextEvent(x11.display, &event); - queued = FALSE; - - switch (event.type) { - case KeyPress: - handle_key_press(&ctx, &x11, event); - break; - - case ButtonPress: - handle_button_press(&ctx, &x11, event); - break; - - case MotionNotify: - handle_motion_notify(&ctx, &x11, event, &queued); - break; - - case Expose: - LOG("[selx]: Expose..." "\n"); - draw(&ctx, &x11, ctx.last, EVENT_MOTION); - break; - - case DestroyNotify: - fatal("received DestroyNotify" "\n"); - break; - - case KeyRelease: /* FALLTHROUGH */ - case MapNotify: /* FALLTHROUGH */ - case ConfigureNotify: - /* nothing lol */ - break; - - default: - LOG("[selx]: Event(%d)..." "\n", event.type); - break; - } - } - - if (features & FEATURE_MAUS) - XUngrabPointer(x11.display, CurrentTime); - - if (features & FEATURE_KEYBOARD) - XUngrabKeyboard(x11.display, CurrentTime); - - // TODO: this executes even when the target window is not visible. - // should we skip in those cases instead? - if (ctx.state == STATE_WINDOW) { - XWindowAttributes window_attributes; - int get_attributes_ret = XGetWindowAttributes( - x11.display, - ctx.target.window, - &window_attributes - ); - - if (get_attributes_ret == 0) - fatal("XGetWindowAttributes() failed"); - - int b = window_attributes.border_width; - ctx.final = (Rect) { - window_attributes.x, window_attributes.y, - window_attributes.width, window_attributes.height - }; - - if (features & FEATURE_WM_BORDER) { - ctx.final.w += b * 2; - ctx.final.h += b * 2; - } else { - ctx.final.x += b; - ctx.final.y += b; - } - ASSERT(features & FEATURE_ROOT); - } else if (ctx.state == STATE_MONITOR) { - int nmonitors; - ASSERT(features & FEATURE_ROOT); - XRRMonitorInfo *monitors = XRRGetMonitors( - x11.display, x11.root.window, True, &nmonitors - ); - - if (monitors == NULL) - fatal("XRRGetMonitors() failed"); - - if (ctx.target.monitor == MONITOR_UNDER_CURSOR) { // clicked - Point *cur = &ctx.last; - for (Size i = 0; i < nmonitors; ++i) { - XRRMonitorInfo *m = monitors + i; - if ( - (cur->x >= m->x && cur->x < (m->x + m->width)) && - (cur->y >= m->y && cur->y < (m->y + m->height)) - ) { - ctx.target.monitor = i; - break; - } - } - } - - if (ctx.target.monitor >= nmonitors) - fatal("no monitor with such index"); - - if (ctx.target.monitor == MONITOR_UNDER_CURSOR) - fatal("couldn't find the monitor under the cursor"); - - ASSERT(ctx.target.monitor >= 0 && ctx.target.monitor < nmonitors); - XRRMonitorInfo *m = monitors + ctx.target.monitor; - ctx.final.x = m->x; - ctx.final.y = m->y; - ctx.final.h = m->height; - ctx.final.w = m->width; - } - - if (ctx.state != STATE_ABORT && ctx.final.w > 0 && ctx.final.h > 0) { - ASSERT( - ctx.state == STATE_WINDOW || - ctx.state == STATE_MONITOR || - ctx.state == STATE_RECT - ); - Rect *r = &ctx.final; - printf("%d %d %d %d" "\n", r->x, r->y, r->w, r->h); - } - - XCloseDisplay(x11.display); - - return 0; -} -

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.