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 5a2b032f41c155acd258b28400ef503474cb4a9b
parent 9b3c57a247e02d09b23d79cf9a615ed7f8db589a
Author: Emma Weaver <emma@waeaves.com>
Date:   Fri, 24 Apr 2026 12:50:30 -0400

added bounds on movement, updated call signature

Diffstat:
M.gitignore1+
MMakefile2+-
MREADME7++++---
Mconfig.draw.h8++++----
Mdraw.c111+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mdraw.h10+++++-----
Mmain.c90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
7 files changed, 152 insertions(+), 77 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -7,3 +7,4 @@ config.h **.swp **.bmp +test-out diff --git a/Makefile b/Makefile @@ -13,7 +13,7 @@ draw.o: draw.c draw.h config.draw.h config.mk cc -c draw.c ${CFLAGS} ${LDFLAGS} -o draw.o clean: - rm -f draw.o bmp.o util.o draw grabs + rm -f draw.o bmp.o util.o draw grabs test-out/*.bmp test-out/*.gif # TODO implement clean and install scripts like this # dist: clean diff --git a/README b/README @@ -11,7 +11,8 @@ Default controls: -------------------------------------------------------------------------- TODO: - - 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 + [ ] add output directory option, save output to dated files + [ ] + [ ] add "install" and "clean" directives like what dwm and st have + [ ] add gif recording diff --git a/config.draw.h b/config.draw.h @@ -13,10 +13,10 @@ const int movement_speed_fast = 12; #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_DOWN_2 XK_s +#define KEY_UP_2 XK_w +#define KEY_RIGHT_2 XK_d +#define KEY_LEFT_2 XK_a #define KEY_MONITOR XK_m #define KEY_WINDOW XK_n #define KEY_TAKE XK_Return diff --git a/draw.c b/draw.c @@ -234,15 +234,15 @@ draw(DrawContext * ctx, X11 * x11) { 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 }; + /* show the active edges thicker */ + if (ctx->keys_down_mask & KEY_DOWN2_MASK) rects[2].h *= 2; + if (ctx->keys_down_mask & KEY_RIGHT2_MASK) rects[3].w *= 2; + if (ctx->keys_down_mask & KEY_LEFT2_MASK) { + rects[1].w *= 2; rects[1].x -= b; + } + if (ctx->keys_down_mask & KEY_UP2_MASK) { + rects[0].h *= 2; rects[0].y -= b; + } /* make the indicator bigger when fast */ if (is_fast(ctx)) { @@ -316,7 +316,6 @@ 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"); @@ -330,10 +329,8 @@ handle_key_press(DrawContext * ctx, X11 * x11, XEvent event) draw(ctx, x11); break; case KEY_TAKE: - { - int shift = ctx->keys_down_mask & KEY_SHIFT_MASK; - ctx->state = shift ? STATE_RECORD : STATE_TAKE; - } + int shift = ctx->keys_down_mask & KEY_SHIFT_MASK; + ctx->state = shift ? STATE_RECORD : STATE_TAKE; break; case KEY_CANCEL: fatal("Cancelled" "\n"); @@ -344,10 +341,10 @@ handle_key_press(DrawContext * ctx, X11 * x11, XEvent event) 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; + case KEY_DOWN_2: ctx->keys_down_mask |= KEY_DOWN2_MASK; break; + case KEY_UP_2: ctx->keys_down_mask |= KEY_UP2_MASK; break; + case KEY_LEFT_2: ctx->keys_down_mask |= KEY_LEFT2_MASK; break; + case KEY_RIGHT_2: ctx->keys_down_mask |= KEY_RIGHT2_MASK; break; } /* draw to update visual hinting */ @@ -368,10 +365,10 @@ handle_key_release(DrawContext * ctx, X11 * x11, XEvent event) 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; + case KEY_DOWN_2: ctx->keys_down_mask &= ~KEY_DOWN2_MASK; break; + case KEY_UP_2: ctx->keys_down_mask &= ~KEY_UP2_MASK; break; + case KEY_LEFT_2: ctx->keys_down_mask &= ~KEY_LEFT2_MASK; break; + case KEY_RIGHT_2: ctx->keys_down_mask &= ~KEY_RIGHT2_MASK; break; } /* draw to update visual hinting */ @@ -398,9 +395,6 @@ handle_motion_notify(DrawContext * ctx, X11 * x11, XEvent event, int * queued) if (ndiscarded) LOG("[selx]: discarded %d stale MotionNotify event" "\n", ndiscarded); - - // ctx->end = p; - // draw(ctx, x11); } void @@ -414,6 +408,7 @@ handle_xevent(DrawContext * ctx, X11 * x11, XEvent event, int * queued) handle_key_release(ctx, x11, event); break; case MotionNotify: + // TODO is this needed? // handle_motion_notify(ctx, x11, event, queued); break; case Expose: @@ -429,36 +424,58 @@ 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; - - 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); + + Point start = ctx->start, end = ctx->end; + + int down = mask & KEY_DOWN_MASK; + int up = mask & KEY_UP_MASK; + int left = mask & KEY_LEFT_MASK; + int right = mask & KEY_RIGHT_MASK; + + int down2 = mask & KEY_DOWN2_MASK; + int up2 = mask & KEY_UP2_MASK; + int left2 = mask & KEY_LEFT2_MASK; + int right2 = mask & KEY_RIGHT2_MASK; + + int shift = mask & KEY_SHIFT_MASK; + int controlled = mask & KEY_CONTROL_MASK; int speed = is_fast(ctx) ? movement_speed_fast : movement_speed_slow; + int delta_x = speed * ((right != 0) - (left != 0)); + int delta_y = speed * ((down != 0) - (up != 0)); + int any_specific = down2 || up2 || right2 || left2; + + int min_x = x11->root.x; + int min_y = x11->root.y; + int max_x = x11->root.x + x11->root.w; + int max_y = x11->root.y + x11->root.h; if (delta_x == 0 && delta_y == 0) return; - if (!any_corners) { + if (!any_specific) { /* 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; + + /* (apply bounds so that the rect stops at screen's edge */ + if(delta_x > 0 && end.x + delta_x > max_x) + delta_x = max_x - end.x; + if(delta_y > 0 && end.y + delta_y > max_y) + delta_y = max_y - end.y; + if(delta_x < 0 && start.x + delta_x < min_x) + delta_x = min_x - start.x; + if(delta_y < 0 && start.y + delta_y < min_y) + delta_y = min_y - start.y; + + ctx->start.x += delta_x; + ctx->start.y += delta_y; + ctx->end.x += delta_x; + ctx->end.y += 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; + /* only move specific sides */ + if (down2) ctx->end.y += delta_y; + if (up2) ctx->start.y += delta_y; + if (right2) ctx->end.x += delta_x; + if (left2) ctx->start.x += delta_x; } draw(ctx, x11); diff --git a/draw.h b/draw.h @@ -12,7 +12,7 @@ #define TRUE 1 #define FALSE 0 -#define WINDOW_COUNT 5 +#define WINDOW_COUNT 4 #define LOG(...) printf(__VA_ARGS__) @@ -32,10 +32,10 @@ enum { 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_UP2_MASK = 1 << 4, + KEY_DOWN2_MASK = 1 << 5, + KEY_LEFT2_MASK = 1 << 6, + KEY_RIGHT2_MASK = 1 << 7, KEY_CONTROL_MASK = 1 << 8, KEY_SHIFT_MASK = 1 << 9 diff --git a/main.c b/main.c @@ -6,6 +6,7 @@ #include <stdlib.h> #include <string.h> #include <unistd.h> +#include <time.h> #include <X11/Xlib.h> #include <X11/Xutil.h> @@ -16,9 +17,17 @@ #define MAX_IMAGE_WIDTH 1366 #define MAX_IMAGE_HEIGHT 768 +#define MAX_DATETIME_LENGTH 100 +#define MAX_FILENAME_LENGTH 200 -const char * usage = "Usage: grabs" "\n"; -uint8_t buffer[MAX_IMAGE_WIDTH * MAX_IMAGE_HEIGHT * 3]; +const char * usage = \ +"Grabs 0.0 🄯 Emma Weaver 2026" "\n\n" \ +"Usage: grabs -d DIR" "\n\n" \ +"Take screenshots and screen recordings with the keyboard." "\n\n"; + +uint8_t image_buffer[MAX_IMAGE_WIDTH * MAX_IMAGE_HEIGHT * 3]; +char datetime_buffer[MAX_DATETIME_LENGTH]; +char filename_buffer[MAX_FILENAME_LENGTH]; void write_screenshot_to_buffer(X11 * x11, Rect target, uint8_t * image_buffer) @@ -51,29 +60,76 @@ write_screenshot_to_buffer(X11 * x11, Rect target, uint8_t * image_buffer) } } +void +populate_filename(const char * directory, const char * extension) +{ + time_t current_time = time(NULL); + strftime( + datetime_buffer, + MAX_DATETIME_LENGTH, + "%F_%T", + localtime(&current_time) + ); + sprintf( + filename_buffer, + "%s/%s.%s", + directory, + datetime_buffer, + extension + ); + printf("Saving to %s..." "\n", filename_buffer); +} + +void +take_bmp_screenshot(X11 * x11, Rect rect) +{ + FILE * stream = fopen(filename_buffer, "w"); + int status; + + /* bmp screenshot */ + write_screenshot_to_buffer(x11, rect, image_buffer); + status = write_bmp(stream, image_buffer, rect.w, rect.h); + if (status != 0) + fatal(get_bmp_status_message(status)); +} + int -main(int argc, const char * argv) +main(int argc, const char * argv[]) { X11 x11; DrawContext ctx = {}; + const char * directory; + + /* print usage if format isn't correct */ + if ( + !( + argc == 3 && + argv[1][0] == '-' && + argv[1][1] == 'd' && + argv[1][2] == '\0' + ) + ) { + printf(usage); + exit(0); + } + /* pick a rectangle */ 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); - - if (status != 0) - fatal(get_bmp_status_message(status)); + Rect rect = get_rectangle(&ctx, &x11); + + /* do output */ + + directory = argv[2]; + if (ctx.state == STATE_TAKE) { + populate_filename(directory, "bmp"); + take_bmp_screenshot(&x11, rect); + } else { + /* gif screen recording */ + populate_filename(directory, "gif"); + fatal("Screen Recording not yet implemented." "\n"); + } XCloseDisplay(x11.display);

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