pax_global_header00006660000000000000000000000064132665342110014515gustar00rootroot0000000000000052 comment=6505bd0dd32fc1b3d84d3f3b4bffa7b07349b429 keynav-master/000077500000000000000000000000001326653421100136475ustar00rootroot00000000000000keynav-master/.gitignore000066400000000000000000000000501326653421100156320ustar00rootroot00000000000000/keynav /keynav.1 /keynav_version.h *.o keynav-master/CHANGELIST000066400000000000000000000222431326653421100152160ustar00rootroot000000000000000.20110708.* - patch from wxs to clean up errors when building with clang. - Fix bug in grid-nav where the column was selected first, when the first letter is that of the row. 0.20101224.* - Fix a bug in how I use strtok_r that causes crashes in FreeBSD. Reported by Richard Kolkovich. 0.20101014.* - Added 'restart' command. Makes keynav restart. Useful for binding a key to reload the config. - Added 'loadconfig' command. This lets you include additional config files to load on the command line or in one of the default keynavrc files. (requested by Axel Beckert) - keynav will now restart if it receives SIGHUP or SIGUSR1 - Map 'Enter' by default to 'warp,click 1,end' (requested by Axel Beckert) by Axel Beckert) - Fix a bug causing the point under the mouse cursor to not click through the keynav window in certain conditions. Reported via mailing list by Eric Van Dewoestine and Krister Svanlund. 0.20100623.* - No functional changes. - Remove monolithic build against libxdo. xdotool is now available in just about every major platform (fedora, ubuntu, archlinux, freebsd, and more) that we don't need to ship with xdotool. 0.20100601.*: - Update to follow API changes in libxdo. Now requires libxdo = 2.x 0.20100403.*: - Use cairo graphics for drawing instead of raw Xlib. This makes drawing a little bit less painful and will allow me to more quickly prototype ideas in the future. - Add 'grid nav' navigation. The original prototype was written by Nazri in 2008. This adds a new command: grid-nav. This command takes one argument, of: toggle, on, off. When on grid navigation, you can select a specific grid by coordinate. The coordinates at this time are A to Z for both row and column. For example, if your grid is 2x2, you can select the top-right cell by typing 'AB' when grid nav is activated. 0.20100302.*: - Started using glib for dynamic arrays (GPtrArray) - Uses new versioning scheme major.date.svnrev - Added 'version' (or -v or --version) to output the keynav version - Now requires libxdo.so.1 (via xdotool) - Add ability to record keynav actions with the 'record' command. Optional argument is a filename to save the recordings to for persistence across keynav runs. Example in keynavrc to bind 'q' to record to ~/.keynav_macros: q record ~/.keynav_macros Works similar to vim recording. 1) Hit record once 2) Type the key you want to record to 3) Do things in keynav you want recorded. 4) Any 'end' operation or hitting record again will terminate and save this recording. Recordings only persist if you specify a file to save to, otherwise they are lost across keynav restarts. 20091231.04: - Try repeatedly to grab the keyboard with XGrabKeyboard on 'start' commands. This loop is a necessary workaround for programs like xbindkeys that could launch keynav but at the time of launch still hold a keyboard grab themselves. Reported by Colin Shea. 20091231.02: - Nonfunctional bug fixes and other code cleanup from patches by Russell Harmon 20091231.01: - Some internal code refactor/cleanup - Reduce drawing flicker by drawing to a Pixmap and blitting to the window. - Allow commands to be given on keynav startup. (Reported by Colin Shea) The same commands valid as keybindings are valid as startup commands: % keynav "start, grid 3x3" - Allow clicking through the keynav grid window area (Reported by Yuri D'Elia) - Support daemonizing using the 'daemonize' command in keynavrc. Added an example to the distributed keynavrc. - Use new library features given by xdotool/libxdo 20091231.01 20091208: - Support linking against libxdo.so if it is found, otherwise we build xdo.o into keynav. The original intent of including xdotool in the release package was to make make it easy to build keynav without a packaging system. Now that more distros have keynav and xdotool, this requirement is less important. This change is in response to Debian rqeuest: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=560103 20091108: - Added xinerama support. * Default 'start' will now only be fullscreen on your current xinerama display. You can move between screens by using the move-* actions to move the current selection outside the border of the current screne. - All xdotool commands now integers so we can forward their return status to the user. - Actually handle SIGCHLD now so the shell commands get reaped on exit. 20080614.01: - Several bug fixes and feature additions suggested by Yuri D'Elia. - Sync xdotool library to 20080606 - Added default key binding Ctrl+[ as 'end' (requested by Luke Macken) - New command: 'sh' - Executes shell commands. Example keynavrc: ctrl+x sh "xterm -bg black -fg white" - New command: 'history-back' - Undo a window change operation Example keynavrc: a history-back + Such operations include: cut-*, grid, cell-select, move-* + The history size is currently hard-coded at 100 entries. + If you exceed 100 moves, the oldest entry will be removed. + Every time keynav is activated, the history is wiped. - Fix: Any command starting with "start" is now bound globally. - Fix: All rendering is delayed until after the end of the current command sequence. This fixes (in order of annoyance, worst first): 1) Crash when a 'start' and 'end' exist in the same command sequence. 2) Visible 2x2 grid first, before a 3x3 grid when the start command is 'start, grid 3x3' 3) Rendering blinking a full white window on the screen before clipping to the grid. 4) Visible blink when "cut-left,cut-up" and such are run simultaneously. - Fix: If the 'start' command is invoked again while keynav is active, then the default arrangement is set (full screen and 2x2 grid). Previously, the 'start' command was a no-op if keynav was active. 20080522: - Sync xdotool library to 20080521. - Added 2 grid examples to keynavrc - Applied patches from Richard Kolkovich + Fix backwards math when calculating Nth cell when using 'cell-select N' + Fix dislexia when doing 'cell-select NxM' + Abort update() calls when app is inactive. - Now warns you if you try to execute an invalid command. 20080509: Feature request: Grid support. * New command: 'grid NxM' N and M are row and columns, respectively. You can divide the screen into any number of rows and columns. The default is 2x2. * New command: 'cell-select N' or 'cell-select NxM' With this command you can select a specific cell to zoom to. Usage: cell-select N Selects the Nth cell, counting from top left to bottom right. The order of a 3x3 grid would be: 1 2 3 4 5 6 7 8 9 Usage: cell-select NxM Selects the specific cell at NxM. '2x2' will select row 2 column 2. Other important changes: - Whitespace before command names works now. - Added a pile of new examples in keynavrc. 20080508: Bug fix: If you tried to override an existing key binding, it would add a 2nd binding for that key instead of actually overriding it. Reported by Tim Schumacher. 20080501: Patches from Mark (20080501) * ~/.keynavrc extends defaults rather than replacing them * "clear" in ~/.keynavrc resets keybindings * comments can appear anywhere on a line Patches from Eric (20080501) * If the move or size value is greater than 1.0, then assume it is an absolute value. Patches from Lukas Mai (20080429) * Fixes a few minor bugs * Clean up to compile without most warnings when -pendantic and -Wall are enabled. 20071031: - Fix support when NumLock/ScrollLock/CapsLock is on. 20071023: - Add support for {Super,Hyper}_{R,L} modifiers (aka Mod4Mask) 20070903: - Drag is now working. Problem was KeyEvent.state contains masks such as | Button1Mask which is set when mouse button 1 is held, so keybindings stopped | working. Ignoring Button[1-5]Mask in this value fixes the problem. - Drag takes two optional arguments: a button followed by a keysequence to fire. | 'drag 1 alt' will do an alt+leftclick drag. | 'drag 2' will do a middleclick drag. - sync to xdotool@20070903 - Fix a bug in parse_mods and parse_keysym where it was destructively changing the string. - Fix a bug where I was using the loop iterator 'i' inside another for loop. Oops. - Add to defaults my nethack-vi-style diagonal keybindings 20070814: - Arguments for {move,cut}_{up,left,down,right} in form of percentage values. Default for cut is 0.5 (cut the window in half) Default for move is 1.0 (move the full width/height of the window) - More examples in distributed keynavrc - sync to xdotool-20070812 20070705: - Report when keysyms or keycodes cannot be looked up for whatever reason. 20070703: - Include COPYRIGHT (bsd license) and a sample keynavrc in the release. 20070629: - Correctly use defaults if no $HOME and no $HOME/.keynavrc is found (patch from wxs) - Clean target is now recursive 20070627: - Config file support. Loads ~/.keynavrc + The config file lets you much more than the original keynav. - Uses xdo for mouse activity - Some drawing fixes keynav-master/COPYRIGHT000066400000000000000000000027131326653421100151450ustar00rootroot00000000000000$Id$ Copyright (c) 2007-2008, Jordan Sissel. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Jordan Sissel nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY JORDAN SISSEL ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JORDAN SISSEL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. keynav-master/DEBIAN000066400000000000000000000001221326653421100145070ustar00rootroot00000000000000packages needed to build: libxinerama-dev libglib2.0-dev libcairo2-dev libxdo-dev keynav-master/Makefile000066400000000000000000000032371326653421100153140ustar00rootroot00000000000000CFLAGS+=$(shell pkg-config --cflags cairo-xlib xinerama glib-2.0 xext x11 xtst 2> /dev/null || echo -I/usr/X11R6/include -I/usr/local/include) LDFLAGS+=$(shell pkg-config --libs cairo-xlib xinerama glib-2.0 xext x11 xtst 2> /dev/null || echo -L/usr/X11R6/lib -L/usr/local/lib -lX11 -lXtst -lXinerama -lXext -lglib) LDFLAGS+=$(shell pkg-config --libs glib-2.0) OTHERFILES=README CHANGELIST COPYRIGHT \ keynavrc Makefile version.sh VERSION #CFLAGS+=-DPROFILE_THINGS #LDFLAGS+=-lrt VERSION=$(shell sh version.sh) #CFLAGS+=-pg -g #LDFLAGS+=-pg -g #LDFLAGS+=-L/usr/lib/debug/usr/lib/ -lcairo -lX11 -lXinerama -LXtst -lXext #CFLAGS+=-O2 #CFLAGS+=-DPROFILE_THINGS #LDFLAGS+=-lrt .PHONY: all all: keynav clean: rm *.o keynav keynav_version.h || true; keynav.o: keynav_version.h keynav_version.h: version.sh keynav: LDFLAGS+=-Xlinker -rpath=/usr/local/lib keynav: keynav.o $(CC) keynav.o -o $@ $(LDFLAGS) -lxdo; \ keynav_version.h: sh version.sh --header > $@ VERSION: sh version.sh --shell > $@ pre-create-package: rm -f keynav_version.h VERSION $(MAKE) VERSION keynav_version.h create-package: clean pre-create-package keynav_version.h NAME=keynav-$(VERSION); \ mkdir $${NAME}; \ rsync --exclude '.*' -av *.c $(OTHERFILES) $${NAME}/; \ tar -zcf $${NAME}.tar.gz $${NAME}/; \ rm -rf $${NAME}/ package: create-package test-package-build test-package-build: create-package @NAME=keynav-$(VERSION); \ tmp=$$(mktemp -d); \ echo "Testing package $$NAME"; \ tar -C $${tmp} -zxf $${NAME}.tar.gz; \ make -C $${tmp}/$${NAME} keynav; \ (cd $${tmp}/$${NAME}; ./keynav version); \ rm -rf $${NAME}/ rm -f $${NAME}.tar.gz keynav.1: keynav.pod pod2man -c "" -r "" $< > $@ keynav-master/README000066400000000000000000000000671326653421100145320ustar00rootroot00000000000000Please see http://www.semicomplete.com/projects/keynav keynav-master/TODO000066400000000000000000000001171326653421100143360ustar00rootroot00000000000000 @ TODO/BUG: windowzoom causes a crash in wmii (Reported by Krister Svanlund) keynav-master/examples/000077500000000000000000000000001326653421100154655ustar00rootroot00000000000000keynav-master/examples/keynavrc.jordan000066400000000000000000000062551326653421100205160ustar00rootroot00000000000000# This is a keynavrc file. Yours should live in # $HOME/.keynavrc # # Lines beginning with '#' are comments. # Format is: # keysequence cmd1,cmd2,cmd3... # # The 'start' command alone is handled specially, in that any key sequence # mapped to 'start' will be grabbed when keynav starts up so you can invoke it # to activate keynav. The remaining keys are only recognized while keynav is # active # # Project page; http://www.semicomplete.com/projects/keynav daemonize # background keynav clear # clear all previous keybindings q record ~/.keynav_macros shift+at playback #ctrl+semicolon start, sh "exec transset-df -i $(xdotool search --class keynav) 0.3" e grid-nav toggle Tab restart ctrl+semicolon start #ctrl+semicolon start,cut-left .95, cut-up .95, move-right 50, move-down 20 alt+semicolon start,move-right Escape end ctrl+bracketleft end h cut-left j cut-down k cut-up l cut-right y cut-left,cut-up u cut-right,cut-up b cut-left,cut-down n cut-right,cut-down shift+h move-left shift+j move-down shift+k move-up shift+l move-right shift+y move-left,move-up shift+u move-right,move-up shift+b move-left,move-down shift+n move-right,move-down space warp,click 1,end semicolon warp,end 1 click 1 2 click 2 3 click 3 #1 cell-select 1x1,warp,end #2 cell-select 1x2,warp,end #3 cell-select 2x1,warp,end #4 cell-select 2x2,warp,end # Zoom to the current window w windowzoom c cursorzoom 100 100 #ctrl+w windowzoom # Handy for holding ctrl while using keynav: ctrl+h cut-left ctrl+j cut-down ctrl+k cut-up ctrl+l cut-right ctrl+y cut-left,cut-up ctrl+u cut-right,cut-up ctrl+b cut-left,cut-down ctrl+n cut-right,cut-down ### Example using the 'sh' command. # This xdotool invocation require xdotool >= 2.20100623 for 'command chaining' g sh "xdotool search --title -- '- Google Chrome' windowactivate key --window 0 --clearmodifiers ctrl+l",end # Paste! v sh "xdotool key shift+Insert",end ctrl+v sh "xdotool key shift+Insert",end # Activate chrome, make a new tab, paste in the url bar, then press return. # This xdotool invocation require xdotool >= 2.20100623 for 'command chaining' t sh "xdotool search --title -- '- Google Chrome' windowactivate --sync key --window 0 --clearmodifiers ctrl+t shift+Insert Return",end ### Drag examples # Start drag holding the left mouse button #q drag 1 # Start drag holding middle mouse + control and shift #w warp ### History a history-back ### Example of cut and move without the default values #h cut-left .75 #j cut-down .75 #k cut-up .75 #l cut-right .75 #shift+h move-left .50 #shift+j move-down .50 #shift+k move-up .50 #shift+l move-right .50 ### Example using a 2-row, 3-column grid, # mapped to Insert/Home/PageUp/etc... #6 grid 2x3 #Insert cell-select 1x1 #Home cell-select 1x2 #Prior cell-select 1x3 # PageUp #Delete cell-select 2x1 #End cell-select 2x2 #Next cell-select 2x3 # PageDown ### Example using a 3x3 grid with nethack-vi keys #ctrl+semicolon start, grid 3x3 #h cell-select 2x1 # left #j cell-select 3x2 # down #k cell-select 1x2 # up #l cell-select 2x3 # right #y cell-select 1x1 # up-left #u cell-select 1x3 # up-right #b cell-select 3x1 # down-left #n cell-select 3x3 # down-right #period cell-select 2x2 # center keynav-master/keynav.c000066400000000000000000001503721326653421100153200ustar00rootroot00000000000000/* * keynav - Keyboard navigation tool. * * XXX: Merge 'wininfo' and 'wininfo_history'. The latest history entry is the * same as wininfo, so use that instead. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef PROFILE_THINGS #include #endif #include #include "keynav_version.h" #ifndef GLOBAL_CONFIG_FILE #define GLOBAL_CONFIG_FILE "/etc/keynavrc" #endif /* GLOBAL_CONFIG_FILE */ extern char **environ; char **g_argv; #define ISACTIVE (appstate.active) #define ISDRAGGING (appstate.dragging) struct appstate { int active; int dragging; int need_draw; int need_moveresize; enum { record_getkey, record_ing, record_off } recording; int playback; int grid_nav; /* 1 if grid nav is active */ enum { GRID_NAV_COL, GRID_NAV_ROW } grid_nav_state; enum { GRID_LABEL_NONE, GRID_LABEL_AA } grid_label; int grid_nav_col; int grid_nav_row; }; typedef enum { HANDLE_CONTINUE, HANDLE_STOP } handler_info_t; typedef struct recording { int keycode; GPtrArray *commands; } recording_t; GPtrArray *recordings; recording_t *active_recording = NULL; char *recordings_filename = NULL; typedef struct wininfo { int x; int y; int w; int h; int grid_rows; int grid_cols; int border_thickness; int center_cut_size; int curviewport; } wininfo_t; typedef struct mouseinfo { int x; int y; } mouseinfo_t; typedef struct viewport { int x; int y; int w; int h; int screen_num; Screen *screen; Window root; } viewport_t; static wininfo_t wininfo; static mouseinfo_t mouseinfo; static viewport_t *viewports; static int nviewports = 0; static int xinerama = 0; static int daemonize = 0; static int is_daemon = False; static Display *dpy; static Window zone; XRectangle *clip_rectangles = NULL; int nclip_rectangles = 0; static GC canvas_gc; static Pixmap canvas; static cairo_surface_t *canvas_surface; static cairo_t *canvas_cairo; static Pixmap shape; static cairo_surface_t *shape_surface; static cairo_t *shape_cairo; static xdo_t *xdo; static struct appstate appstate = { .active = 0, .dragging = 0, .recording = record_off, .grid_nav = 0, }; static int drag_button = 0; static char drag_modkeys[128]; /* history tracking */ #define WININFO_MAXHIST (100) static wininfo_t wininfo_history[WININFO_MAXHIST]; /* XXX: is 100 enough? */ static int wininfo_history_cursor = 0; void defaults(); void cmd_cell_select(char *args); void cmd_click(char *args); void cmd_cursorzoom(char *args); void cmd_cut_down(char *args); void cmd_cut_left(char *args); void cmd_cut_right(char *args); void cmd_cut_up(char *args); void cmd_daemonize(char *args); void cmd_doubleclick(char *args); void cmd_drag(char *args); void cmd_end(char *args); void cmd_grid(char *args); void cmd_grid_nav(char *args); void cmd_history_back(char *args); void cmd_loadconfig(char *args); void cmd_move_down(char *args); void cmd_move_left(char *args); void cmd_move_right(char *args); void cmd_move_up(char *args); void cmd_quit(char *args); void cmd_record(char *args); void cmd_playback(char *args); void cmd_restart(char *args); void cmd_shell(char *args); void cmd_start(char *args); void cmd_warp(char *args); void cmd_windowzoom(char *args); void update(); void correct_overflow(); void handle_keypress(XKeyEvent *e); void handle_commands(char *commands); void parse_config(); int parse_config_line(char *line); void save_history_point(); void restore_history_point(int moves_ago); void cell_select(int x, int y); handler_info_t handle_recording(XKeyEvent *e); handler_info_t handle_gridnav(XKeyEvent *e); void query_screens(); void query_screen_xinerama(); void query_screen_normal(); int viewport_sort(const void *a, const void *b); int query_current_screen(); int query_current_screen_xinerama(); int query_current_screen_normal(); void viewport_left(); void viewport_right(); int pointinrect(int px, int py, int rx, int ry, int rw, int rh); int percent_of(int num, char *args, float default_val); void sigchld(int sig); void sighup(int sig); void restart(); void recordings_save(const char *filename); void parse_recordings(const char *filename); void openpixel(Display *dpy, Window zone, mouseinfo_t *mouseinfo); void closepixel(Display *dpy, Window zone, mouseinfo_t *mouseinfo); typedef struct dispatch { char *command; void (*func)(char *args); } dispatch_t; dispatch_t dispatch[] = { "cut-up", cmd_cut_up, "cut-down", cmd_cut_down, "cut-left", cmd_cut_left, "cut-right", cmd_cut_right, "move-up", cmd_move_up, "move-down", cmd_move_down, "move-left", cmd_move_left, "move-right", cmd_move_right, "cursorzoom", cmd_cursorzoom, "windowzoom", cmd_windowzoom, // Grid commands "grid", cmd_grid, "grid-nav", cmd_grid_nav, "cell-select", cmd_cell_select, // Mouse activity "warp", cmd_warp, "click", cmd_click, "doubleclick", cmd_doubleclick, "drag", cmd_drag, // Other commands. "loadconfig", cmd_loadconfig, "daemonize", cmd_daemonize, "sh", cmd_shell, "start", cmd_start, "end", cmd_end, "history-back", cmd_history_back, "quit", cmd_quit, "restart", cmd_restart, "record", cmd_record, "playback", cmd_playback, NULL, NULL, }; typedef struct keybinding { char *commands; int keycode; int mods; } keybinding_t; GPtrArray *keybindings = NULL; int startKeycode = 0; int startKeymods = 0; int parse_keycode(char *keyseq) { char *tokctx; char *strptr; char *tok; char *last_tok; char *dup; int keycode = 0; int keysym = 0; strptr = dup = strdup(keyseq); //printf("finding keycode for %s\n", keyseq); while ((tok = strtok_r(strptr, "+", &tokctx)) != NULL) { last_tok = tok; strptr = NULL; } keysym = XStringToKeysym(last_tok); if (keysym == NoSymbol) { fprintf(stderr, "No keysym found for '%s' in sequence '%s'\n", last_tok, keyseq); /* At this point, we'll be returning 0 for keycode */ } else { /* Valid keysym */ keycode = XKeysymToKeycode(dpy, keysym); if (keycode == 0) { fprintf(stderr, "Unable to lookup keycode for %s\n", last_tok); } } free(dup); return keycode; } int parse_mods(char *keyseq) { char *tokctx; char *strptr; char *tok; char *last_tok; char *dup; GPtrArray *mods; int modmask = 0; mods = g_ptr_array_new(); strptr = dup = strdup(keyseq); while ((tok = strtok_r(strptr, "+", &tokctx)) != NULL) { strptr = NULL; g_ptr_array_add(mods, tok); } int i = 0; /* Use all but the last token as modifiers */ const char **symbol_map = xdo_get_symbol_map(); for (i = 0; i < mods->len; i++) { KeySym keysym = 0; int j = 0; const char *mod = g_ptr_array_index(mods, i); for (j = 0; symbol_map[j] != NULL; j+=2) { if (!strcasecmp(mod, symbol_map[j])) { mod = symbol_map[j + 1]; } } keysym = XStringToKeysym(mod); //printf("%s => %d\n", mod, keysym); if ((keysym == XK_Shift_L) || (keysym == XK_Shift_R)) modmask |= ShiftMask; if ((keysym == XK_Control_L) || (keysym == XK_Control_R)) modmask |= ControlMask; if ((keysym == XK_Alt_L) || (keysym == XK_Alt_R)) modmask |= Mod1Mask; if ((keysym == XK_Super_L) || (keysym == XK_Super_R) || (keysym == XK_Hyper_L) || (keysym == XK_Hyper_R)) modmask |= Mod4Mask; /* 'xmodmap' will output the current modN:KeySym mappings */ } free(dup); g_ptr_array_free(mods, FALSE); return modmask; } void addbinding(int keycode, int mods, char *commands) { int i; // Check if we already have a binding for this, if so, override it. for (i = 0; i < keybindings->len; i++) { keybinding_t *kbt = g_ptr_array_index(keybindings, i); if (kbt->keycode == keycode && kbt->mods == mods) { free(kbt->commands); kbt->commands = strdup(commands); return; } } keybinding_t *keybinding = NULL; keybinding = calloc(sizeof(keybinding_t), 1); keybinding->commands = strdup(commands); keybinding->keycode = keycode; keybinding->mods = mods; g_ptr_array_add(keybindings, keybinding); if (!strncmp(commands, "start", 5)) { int i = 0; startKeycode = keycode; startKeymods = mods; /* Grab on all screen root windows */ for (i = 0; i < ScreenCount(dpy); i++) { Window root = RootWindow(dpy, i); XGrabKey(dpy, keycode, mods, root, False, GrabModeAsync, GrabModeAsync); XGrabKey(dpy, keycode, mods | LockMask, root, False, GrabModeAsync, GrabModeAsync); XGrabKey(dpy, keycode, mods | Mod2Mask, root, False, GrabModeAsync, GrabModeAsync); XGrabKey(dpy, keycode, mods | LockMask | Mod2Mask, root, False, GrabModeAsync, GrabModeAsync); } } if (!strncmp(commands, "record", 6)) { char *path = commands + 6; char *newrecordingpath; while (isspace(*path)) path++; /* If args is nonempty, try to use it as the file to store recordings in */ if (path != NULL && path[0] != '\0') { /* Handle ~/ swapping in for actual homedir */ if (!strncmp(path, "~/", 2)) { asprintf(&newrecordingpath, "%s/%s", getenv("HOME"), path + 2); } else { newrecordingpath = strdup(path); } /* Fail if we try to set the record file to another name than we set * previously */ if (recordings_filename != NULL && strcmp(recordings_filename, newrecordingpath)) { free(newrecordingpath); fprintf(stderr, "Recordings file already set to '%s', you tried to\n" "set it to '%s'. Keeping original value.\n", recordings_filename, path); } else { recordings_filename = newrecordingpath; parse_recordings(recordings_filename); } } } /* special config handling for 'record' */ } void parse_config_file(const char* file) { FILE *fp = NULL; #define LINEBUF_SIZE 512 char line[LINEBUF_SIZE]; int lineno = 0; if (file[0] == '~') { const char *homedir = getenv("HOME"); if (homedir != NULL) { char *rcfile = NULL; asprintf(&rcfile, "%s/%s", homedir, file + 1 /* skip first char '~' */); parse_config_file(rcfile); free(rcfile); return; } else { fprintf(stderr, "No HOME set in environment. Can't expand '%s' (fatal error)\n", file); /* This is fatal. */ exit(1); } } /* if file[0] == '~' */ fp = fopen(file, "r"); /* Silently ignore file read errors */ if (fp == NULL) { //fprintf(stderr, "Error trying to open file for read '%s'\n", file); //perror("Error"); return; } /* fopen succeeded */ while (fgets(line, LINEBUF_SIZE, fp) != NULL) { lineno++; /* Kill the newline */ *(line + strlen(line) - 1) = '\0'; if (parse_config_line(line) != 0) { fprintf(stderr, "Error with config %s:%d: %s\n", file, lineno, line); } } fclose(fp); } void parse_config() { char *homedir; keybindings = g_ptr_array_new(); recordings = g_ptr_array_new(); defaults(); parse_config_file(GLOBAL_CONFIG_FILE); parse_config_file("~/.keynavrc"); } void defaults() { char *tmp; int i; char *default_config[] = { "clear", "ctrl+semicolon start", "Escape end", "ctrl+bracketleft end", /* for vi people who use ^[ */ "q record ~/.keynav_macros", "shift+at playback", "a history-back", "h cut-left", "j cut-down", "k cut-up", "l cut-right", "shift+h move-left", "shift+j move-down", "shift+k move-up", "shift+l move-right", "space warp,click 1,end", "Return warp,click 1,end", "semicolon warp,end", "w warp", "t windowzoom", "c cursorzoom 300 300", "e end", "1 click 1", "2 click 2", "3 click 3", "ctrl+h cut-left", "ctrl+j cut-down", "ctrl+k cut-up", "ctrl+l cut-right", "y cut-left,cut-up", "u cut-right,cut-up", "b cut-left,cut-down", "n cut-right,cut-down", "shift+y move-left,move-up", "shift+u move-right,move-up", "shift+b move-left,move-down", "shift+n move-right,move-down", "ctrl+y cut-left,cut-up", "ctrl+u cut-right,cut-up", "ctrl+b cut-left,cut-down", "ctrl+n cut-right,cut-down", NULL, }; for (i = 0; default_config[i]; i++) { tmp = strdup(default_config[i]); if (parse_config_line(tmp) != 0) { fprintf(stderr, "Error with default config line %d: %s\n", i + 1, tmp); } free(tmp); } } int parse_config_line(char *orig_line) { /* syntax: * keysequence cmd1,cmd2,cmd3 * * ex: * ctrl+semicolon start * space warp * semicolon warp,click */ char *line = strdup(orig_line); char *tokctx; char *keyseq; int keycode, mods; char *comment; /* Ignore everything after a '#' */ comment = strchr(line, '#'); if (comment != NULL) *comment = '\0'; /* Ignore leading whitespace */ while (isspace(*line)) line++; /* Ignore empty lines */ if (*line == '\n' || *line == '\0') return 0; tokctx = line; keyseq = strdup(strtok_r(line, " ", &tokctx)); /* A special config option that will clear all keybindings */ if (strcmp(keyseq, "clear") == 0) { /* TODO(sissel): Make this a cmd_clear function */ /* Reset keybindings */ g_ptr_array_free(keybindings, TRUE); keybindings = g_ptr_array_new(); if(startKeycode != 0){ int i; for (i = 0; i < ScreenCount(dpy); i++) { Window root = RootWindow(dpy, i); XUngrabKey(dpy, startKeycode, startKeymods, root); XUngrabKey(dpy, startKeycode, startKeymods | LockMask, root); XUngrabKey(dpy, startKeycode, startKeymods | Mod2Mask, root); XUngrabKey(dpy, startKeycode, startKeymods | LockMask | Mod2Mask, root); } startKeycode = startKeymods = 0; } } else if (strcmp(keyseq, "daemonize") == 0) { handle_commands(keyseq); } else if (strcmp(keyseq, "loadconfig") == 0) { handle_commands(keyseq); } else { keycode = parse_keycode(keyseq); if (keycode == 0) { fprintf(stderr, "Problem parsing keysequence '%s'\n", keyseq); return 1; } mods = parse_mods(keyseq); /* FreeBSD sets 'tokctx' to NULL at end of string. * glibc sets 'tokctx' to the next character (the '\0') * Reported by Richard Kolkovich */ if (tokctx == NULL || *tokctx == '\0') { fprintf(stderr, "Incomplete configuration line. Missing commands: '%s'\n", line); return 1; } addbinding(keycode, mods, tokctx /* the remainder of the line */); } free(keyseq); free(line); return 0; } int percent_of(int num, char *args, float default_val) { static float precision = 100000.0; float pct = 0.0; int value = 0; /* Parse a float. If this fails, assume the default value */ if (sscanf(args, "%f", &pct) <= 0) pct = default_val; /* > 1, then it's not a percent, it's an absolute value. */ if (pct > 1.0) return (int)pct; value = (int)((num * (pct * precision)) / precision); return value; } void updatecliprects(wininfo_t *info, XRectangle **rectangles, int *nrects) { int rects = (info->grid_cols + 1) + (info->grid_rows + 1) /* grid lines */ + (info->grid_cols * info->grid_rows); /* grid text boxes */ if (rects != nclip_rectangles) { nclip_rectangles = rects; clip_rectangles = realloc(clip_rectangles, nclip_rectangles * sizeof(XRectangle)); } } void updategrid(Window win, struct wininfo *info, int apply_clip, int draw) { double w = info->w; double h = info->h; double cell_width; double cell_height; double x_off, y_off; int i; int rect = 0; if (apply_clip) { updatecliprects(info, &clip_rectangles, &nclip_rectangles); memset(clip_rectangles, 0, nclip_rectangles * sizeof(XRectangle)); } #ifdef PROFILE_THINGS struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); #endif //printf("updategrid: clip:%d, draw:%d\n", apply_clip, draw); x_off = info->border_thickness / 2; y_off = info->border_thickness / 2; if (draw) { cairo_new_path(canvas_cairo); cairo_set_source_rgb(canvas_cairo, 1, 1, 1); cairo_rectangle(canvas_cairo, 0, 0, w, h); cairo_fill(canvas_cairo); cairo_set_line_width(canvas_cairo, wininfo.border_thickness); } w -= info->border_thickness; h -= info->border_thickness; cell_width = (w / info->grid_cols); cell_height = (h / info->grid_rows); /* clip vertically */ for (i = 0; i <= info->grid_cols; i++) { cairo_move_to(canvas_cairo, cell_width * i + x_off, y_off); cairo_line_to(canvas_cairo, cell_width * i + x_off, h + 1); clip_rectangles[rect].x = cell_width * i; clip_rectangles[rect].y = 0; clip_rectangles[rect].width = info->border_thickness; clip_rectangles[rect].height = info->h; rect++; } /* clip horizontally */ for (i = 0; i <= info->grid_rows; i++) { cairo_move_to(canvas_cairo, x_off, cell_height * i + y_off); cairo_line_to(canvas_cairo, w + 1, cell_height * i + y_off); clip_rectangles[rect].x = 0; clip_rectangles[rect].y = cell_height * i; clip_rectangles[rect].width = info->w; clip_rectangles[rect].height = info->border_thickness; rect++; } cairo_path_t *path = cairo_copy_path(canvas_cairo); #ifdef PROFILE_THINGS clock_gettime(CLOCK_MONOTONIC, &end); printf("updategrid pathbuild time: %ld.%09ld\n", end.tv_sec - start.tv_sec, end.tv_nsec - start.tv_nsec); clock_gettime(CLOCK_MONOTONIC, &start); #endif if (draw) { cairo_set_source_rgb(canvas_cairo, 0.5, 0, 0); cairo_set_line_width(canvas_cairo, 3); cairo_stroke(canvas_cairo); /* cairo_stroke clears the current path, put it back */ cairo_append_path(canvas_cairo, path); cairo_set_line_width(canvas_cairo, 1); cairo_set_source_rgba(canvas_cairo, 1, 1, 1, .7); cairo_stroke(canvas_cairo); #ifdef PROFILE_THINGS XSync(dpy, False); clock_gettime(CLOCK_MONOTONIC, &end); printf("updategrid draw time: %ld.%09ld\n", end.tv_sec - start.tv_sec, end.tv_nsec - start.tv_nsec); clock_gettime(CLOCK_MONOTONIC, &start); #endif } /* if draw */ cairo_path_destroy(path); } void updategridtext(Window win, struct wininfo *info, int apply_clip, int draw) { double w = info->w; double h = info->h; double cell_width; double cell_height; double x_off, y_off; int row, col; int rect = (info->grid_cols + 1 + info->grid_rows + 1); /* start at end of grid lines */ x_off = info->border_thickness / 2; y_off = info->border_thickness / 2; cairo_text_extents_t te; #define FONTSIZE 18 if (draw) { cairo_new_path(canvas_cairo); cairo_select_font_face(canvas_cairo, "Courier", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD); cairo_set_font_size(canvas_cairo, FONTSIZE); cairo_text_extents(canvas_cairo, "AA", &te); } w -= info->border_thickness; h -= info->border_thickness; cell_width = (w / info->grid_cols); cell_height = (h / info->grid_rows); h++; w++; //printf("bearing: %f,%f\n", te.x_bearing, te.y_bearing); //printf("size: %f,%f\n", te.width, te.height); char label[3] = "AA"; int row_selected = 0; for (col = 0; col < info->grid_cols; col++) { label[0] = 'A'; for (row = 0; row < info->grid_rows; row++) { int rectwidth = te.width + 25; int rectheight = te.height + 8; int xpos = cell_width * col + x_off + (cell_width / 2); int ypos = cell_height * row + y_off + (cell_height / 2); row_selected = (appstate.grid_nav && appstate.grid_nav_row == row && appstate.grid_nav_state == GRID_NAV_COL); //printf("Grid: %c%c\n", label[0], label[1]); /* If the current column is the one selected by grid nav, use * a different color */ //printf("Grid geom: %fx%f @ %d,%d\n", //xpos - rectwidth / 2 + te.x_bearing / 2, //ypos - rectheight / 2 + te.y_bearing / 2, //rectwidth, rectheight); cairo_rectangle(canvas_cairo, xpos - rectwidth / 2 + te.x_bearing / 2, ypos - rectheight / 2 + te.y_bearing / 2, rectwidth, rectheight); if (draw) { cairo_path_t *pathcopy; pathcopy = cairo_copy_path(canvas_cairo); cairo_set_line_width(shape_cairo, 2); if (row_selected) { cairo_set_source_rgb(canvas_cairo, 0, .3, .3); } else { cairo_set_source_rgb(canvas_cairo, 0, .2, 0); } cairo_fill(canvas_cairo); cairo_append_path(canvas_cairo, pathcopy); cairo_set_source_rgb(canvas_cairo, .8, .8, 0); cairo_stroke(canvas_cairo); cairo_path_destroy(pathcopy); if (row_selected) { cairo_set_source_rgb(canvas_cairo, 1, 1, 1); } else { cairo_set_source_rgb(canvas_cairo, .8, .8, .8); } cairo_fill(canvas_cairo); cairo_move_to(canvas_cairo, xpos - te.width / 2, ypos); cairo_show_text(canvas_cairo, label); } if (apply_clip) { clip_rectangles[rect].x = xpos - rectwidth / 2 + te.x_bearing / 2; clip_rectangles[rect].y = ypos - rectheight / 2 + te.y_bearing / 2; clip_rectangles[rect].width = rectwidth + 1; clip_rectangles[rect].height = rectheight + 1; rect++; } label[0]++; } label[1]++; } /* Draw rectangles and text */ } /* void updategridtext */ void cmd_start(char *args) { XSetWindowAttributes winattr; int i; int screen; screen = query_current_screen(); wininfo.curviewport = screen; appstate.grid_nav_row = -1; appstate.grid_nav_col = -1; wininfo.x = viewports[wininfo.curviewport].x; wininfo.y = viewports[wininfo.curviewport].y; wininfo.w = viewports[wininfo.curviewport].w; wininfo.h = viewports[wininfo.curviewport].h; /* Default start with 4 cells, 2x2 */ wininfo.grid_rows = 2; wininfo.grid_cols = 2; wininfo.border_thickness = 3; wininfo.center_cut_size = 3; if (ISACTIVE) return; int depth; int grabstate; int grabtries = 0; /* This loop is to work around the following scenario: * xbindkeys invokes XGrabKeyboard when you press a bound keystroke and * doesn't Ungrab until you release a key. * Example: (xbindkey '(Control semicolon) "keynav 'start, grid 2x2'") * This will only invoke XUngrabKeyboard when you release 'semicolon' * * The problem is that keynav would be launched as soon as the keydown * event 'control + semicolon' occurs, but we could only get the grab on * the release. * * This sleepyloop will keep trying to grab the keyboard until it succeeds. * * Reported by Colin Shea */ grabstate = XGrabKeyboard(dpy, viewports[wininfo.curviewport].root, False, GrabModeAsync, GrabModeAsync, CurrentTime); while (grabstate != GrabSuccess) { usleep(10000); /* sleep for 10ms */ grabstate = XGrabKeyboard(dpy, viewports[wininfo.curviewport].root, False, GrabModeAsync, GrabModeAsync, CurrentTime); grabtries += 1; if (grabtries >= 20) { fprintf(stderr, "XGrabKeyboard failed %d times, giving up...\n", grabtries); /* Returning from here will result in the appstate.active still * being false. */ return; } } //printf("Got grab!\n"); appstate.active = True; appstate.need_draw = 1; appstate.need_moveresize = 1; if (zone == 0) { /* Create our window for the first time */ viewport_t *viewport = &(viewports[wininfo.curviewport]); depth = viewports[wininfo.curviewport].screen->root_depth; wininfo_history_cursor = 0; zone = XCreateSimpleWindow(dpy, viewport->root, wininfo.x, wininfo.y, wininfo.w, wininfo.h, 0, 0, 0); xdo_set_window_class(xdo, zone, "keynav", "keynav"); canvas_gc = XCreateGC(dpy, zone, 0, NULL); canvas = XCreatePixmap(dpy, zone, viewport->w, viewport->h, viewport->screen->root_depth); canvas_surface = cairo_xlib_surface_create(dpy, canvas, viewport->screen->root_visual, viewport->w, viewport->h); canvas_cairo = cairo_create(canvas_surface); cairo_set_antialias(canvas_cairo, CAIRO_ANTIALIAS_NONE); cairo_set_line_cap(canvas_cairo, CAIRO_LINE_CAP_SQUARE); shape = XCreatePixmap(dpy, zone, viewport->w, viewport->h, 1); shape_surface = cairo_xlib_surface_create_for_bitmap(dpy, shape, viewport->screen, viewport->w, viewport->h); shape_cairo = cairo_create(shape_surface); cairo_set_line_width(shape_cairo, wininfo.border_thickness); cairo_set_antialias(canvas_cairo, CAIRO_ANTIALIAS_NONE); cairo_set_line_cap(shape_cairo, CAIRO_LINE_CAP_SQUARE); /* Tell the window manager not to manage us */ winattr.override_redirect = 1; XChangeWindowAttributes(dpy, zone, CWOverrideRedirect, &winattr); XSelectInput(dpy, zone, StructureNotifyMask | ExposureMask | PointerMotionMask | LeaveWindowMask ); } /* if zone == 0 */ } void cmd_end(char *args) { if (!ISACTIVE) return; /* kill drag state too */ if (ISDRAGGING) cmd_drag(NULL); /* Stop recording if we're in that mode */ if (appstate.recording != record_off) { cmd_record(NULL); } appstate.active = False; //XDestroyWindow(dpy, zone); XUnmapWindow(dpy, zone); XUngrabKeyboard(dpy, CurrentTime); } void cmd_history_back(char *args) { if (!ISACTIVE) return; restore_history_point(1); } void cmd_loadconfig(char *args) { // Trim leading and trailing quotes if they exist if (*args == '"') { args++; *(args + strlen(args) - 1) = '\0'; } parse_config_file(args); } void cmd_shell(char *args) { // Trim leading and trailing quotes if they exist if (*args == '"') { args++; *(args + strlen(args) - 1) = '\0'; } if (fork() == 0) { /* child */ int ret; char *const shell = "/bin/sh"; char *const argv[4] = { shell, "-c", args, NULL }; //printf("Exec: %s\n", args); //printf("Shell: %s\n", shell); ret = execvp(shell, argv); perror("execve"); exit(1); } } void cmd_quit(char *args) { exit(0); } void cmd_restart(char *args) { restart(); } void cmd_cut_up(char *args) { if (!ISACTIVE) return; wininfo.h = percent_of(wininfo.h, args, .5); } void cmd_cut_down(char *args) { if (!ISACTIVE) return; int orig = wininfo.h; wininfo.h = percent_of(wininfo.h, args, .5); wininfo.y += orig - wininfo.h; } void cmd_cut_left(char *args) { if (!ISACTIVE) return; wininfo.w = percent_of(wininfo.w, args, .5); } void cmd_cut_right(char *args) { int orig = wininfo.w; if (!ISACTIVE) return; wininfo.w = percent_of(wininfo.w, args, .5); wininfo.x += orig - wininfo.w; } void cmd_move_up(char *args) { if (!ISACTIVE) return; wininfo.y -= percent_of(wininfo.h, args, 1); } void cmd_move_down(char *args) { if (!ISACTIVE) return; wininfo.y += percent_of(wininfo.h, args, 1); } void cmd_move_left(char *args) { if (!ISACTIVE) return; wininfo.x -= percent_of(wininfo.w, args, 1); } void cmd_move_right(char *args) { if (!ISACTIVE) return; wininfo.x += percent_of(wininfo.w, args, 1); } void cmd_cursorzoom(char *args) { int xradius = 0, yradius = 0, width = 0, height = 0; int xloc, yloc; if (!ISACTIVE) return; int count = sscanf(args, "%d %d", &width, &height); if (count == 0) { fprintf(stderr, "Invalid usage of 'cursorzoom' (expected at least 1 argument)\n"); } else if (count == 1) { /* If only one argument, assume we want a square. */ height = width; } xdo_get_mouse_location(xdo, &xloc, &yloc, NULL); wininfo.x = xloc - (width / 2); wininfo.y = yloc - (height / 2); wininfo.w = width; wininfo.h = height; } void cmd_windowzoom(char *args) { Window curwin; Window rootwin; Window dummy_win; int x, y; unsigned int width, height, border_width, depth; xdo_get_active_window(xdo, &curwin); XGetGeometry(xdo->xdpy, curwin, &rootwin, &x, &y, &width, &height, &border_width, &depth); XTranslateCoordinates(xdo->xdpy, curwin, rootwin, -border_width, -border_width, &x, &y, &dummy_win); wininfo.x = x; wininfo.y = y; wininfo.w = width; wininfo.h = height; } void cmd_warp(char *args) { if (!ISACTIVE) return; int x, y; x = wininfo.x + wininfo.w / 2; y = wininfo.y + wininfo.h / 2; if (mouseinfo.x != -1 && mouseinfo.y != -1) { closepixel(dpy, zone, &mouseinfo); } /* Open pixels hould be relative to the window coordinates, * not screen coordinates. */ mouseinfo.x = x - wininfo.x; mouseinfo.y = y - wininfo.y; openpixel(dpy, zone, &mouseinfo); xdo_move_mouse(xdo, x, y, viewports[wininfo.curviewport].screen_num); xdo_wait_for_mouse_move_to(xdo, x, y); /* TODO(sissel): do we need to open again? */ openpixel(dpy, zone, &mouseinfo); } void cmd_click(char *args) { if (!ISACTIVE) return; int button; button = atoi(args); if (button > 0) xdo_click_window(xdo, CURRENTWINDOW, button); else fprintf(stderr, "Negative mouse button is invalid: %d\n", button); } void cmd_doubleclick(char *args) { if (!ISACTIVE) return; cmd_click(args); cmd_click(args); } void cmd_drag(char *args) { if (!ISACTIVE) return; int button; if (args == NULL) { button = drag_button; } else { int count = sscanf(args, "%d %127s", &button, drag_modkeys); if (count == 0) { button = 1; /* Default to left mouse button */ drag_modkeys[0] = '\0'; } else if (count == 1) { drag_modkeys[0] = '\0'; } } if (button <= 0) { fprintf(stderr, "Negative or no mouse button given. Not valid. Button read was '%d'\n", button); return; } drag_button = button; if (ISDRAGGING) { /* End dragging */ appstate.dragging = False; xdo_mouse_up(xdo, CURRENTWINDOW, button); } else { /* Start dragging */ cmd_warp(NULL); appstate.dragging = True; xdo_send_keysequence_window_down(xdo, 0, drag_modkeys, 12000); xdo_mouse_down(xdo, CURRENTWINDOW, button); /* Sometimes we need to move a little to tell the app we're dragging */ /* TODO(sissel): Make this a 'mousewiggle' command */ xdo_move_mouse_relative(xdo, 1, 0); xdo_move_mouse_relative(xdo, -1, 0); XSync(xdo->xdpy, 0); xdo_send_keysequence_window_up(xdo, 0, drag_modkeys, 12000); } } void cmd_grid_nav(char *args) { if (!strcmp("on", args)) { appstate.grid_label = GRID_LABEL_AA; } else if (!strcmp("off", args)) { appstate.grid_label = GRID_LABEL_NONE; } else if (!strcmp("toggle", args)) { if (appstate.grid_label == GRID_LABEL_NONE) { appstate.grid_label = GRID_LABEL_AA; } else { appstate.grid_label = GRID_LABEL_NONE; } } /* Set state grid_nav if grid_label is anything but NONE */ if (appstate.grid_label == GRID_LABEL_NONE) { appstate.grid_nav = 0; } else { appstate.grid_nav = 1; appstate.grid_nav_state = GRID_NAV_ROW; } appstate.need_draw = 1; } void cmd_grid(char *args) { int grid_cols, grid_rows; // Try to parse 'NxN' where N is a number. if (sscanf(args, "%dx%d", &grid_cols, &grid_rows) <= 0) { // Otherwise, try parsing a number. grid_cols = grid_rows = atoi(args); } if (grid_cols <= 0 || grid_rows <= 0) { fprintf(stderr, "Invalid grid segmentation: %dx%d\n", grid_cols, grid_rows); fprintf(stderr, "Grid x and y must both be greater than 0.\n"); return; } wininfo.grid_cols = grid_cols; wininfo.grid_rows = grid_rows; } void cmd_cell_select(char *args) { int row, col, z; int cell_width, cell_height; row = col = z = 0; // Try to parse 'NxM' where N and M are a number. if (sscanf(args, "%dx%d", &col, &row) < 2) { // Otherwise, try parsing just number. z = atoi(args); } // if z > 0, then this means we said "cell-select N" if (z > 0) { double dx = (double)z / (double)wininfo.grid_rows; col = (z / wininfo.grid_rows); if ( (double)col != dx ) { col++; } row = (z % wininfo.grid_rows); if ( 0 == row ) { row = wininfo.grid_rows; } } if (col <= 0 && row <= 0) { fprintf(stderr, "Cell number cannot be zero or negative. I was given" "columns=%d and rows=%d\n", col, row); return; } if (col > wininfo.grid_cols && row > wininfo.grid_rows) { fprintf(stderr, "The active grid is %dx%d, and you selected %dx%d which " "does not exist.\n", wininfo.grid_cols, wininfo.grid_rows, col, row); return; } // else, then we said cell-select NxM // cell_selection is 0-based, so subtract 1. cell_select(col - 1, row - 1); } void cell_select(int col, int row) { //printf("cell_select: %d, %d\n", col, row); wininfo.w = wininfo.w / wininfo.grid_cols; wininfo.h = wininfo.h / wininfo.grid_rows; wininfo.x = wininfo.x + (wininfo.w * (col)); wininfo.y = wininfo.y + (wininfo.h * (row)); } void cmd_daemonize(char *args) { if (!is_daemon) { daemonize = 1; } } void cmd_playback(char *args) { appstate.playback = 1; } void cmd_record(char *args) { char *filename; if (!ISACTIVE) return; if (appstate.recording != record_off) { appstate.recording = record_off; g_ptr_array_add(recordings, (gpointer) active_recording); /* Save to file */ if (recordings_filename != NULL) { recordings_save(recordings_filename); } } else { active_recording = calloc(sizeof(recording_t), 1); active_recording->commands = g_ptr_array_new(); appstate.recording = record_getkey; } } void update() { if (!ISACTIVE) return; /* Fix keynav window boundaries if they exceed the screen */ correct_overflow(); if (wininfo.w <= 1 || wininfo.h <= 1) { cmd_end(NULL); return; } wininfo_t *previous = &(wininfo_history[wininfo_history_cursor - 1]); //printf("window: %d,%d @ %d,%d\n", wininfo.w, wininfo.h, wininfo.x, wininfo.y); //printf("previous: %d,%d @ %d,%d\n", previous->w, previous->h, previous->x, previous->y); int draw = 0, move = 0, resize = 0, clip = 0; if (previous->x != wininfo.x || previous->y != wininfo.y) { move = 1; } if (previous->w != wininfo.w || previous->h != wininfo.h) { clip = 1; draw = 1; resize = 1; } if (appstate.need_draw) { clip = 1; draw = 1; appstate.need_draw = 0; } if (appstate.need_moveresize) { move = 1; resize = 1; appstate.need_moveresize = 0; } //printf("move: %d, clip: %d, draw: %d, resize: %d\n", move, clip, draw, resize); //clip = 0; if (((clip || draw) + (move || resize)) > 1) { /* more than one action to perform, unmap to hide move/draws * to reduce flickering */ XUnmapWindow(dpy, zone); } if (clip || draw) { updategrid(zone, &wininfo, clip, draw); if (appstate.grid_label != GRID_LABEL_NONE) { updategridtext(zone, &wininfo, clip, draw); } if (draw) { XCopyArea(dpy, canvas, zone, canvas_gc, 0, 0, wininfo.w, wininfo.h, 0, 0); } if (clip) { XShapeCombineRectangles(dpy, zone, ShapeBounding, 0, 0, clip_rectangles, nclip_rectangles, ShapeSet, 0); } } if (resize && move) { //printf("=> %ld: %dx%d @ %d,%d\n", zone, wininfo.w, wininfo.h, wininfo.x, //wininfo.y); XMoveResizeWindow(dpy, zone, wininfo.x, wininfo.y, wininfo.w, wininfo.h); /* Under Gnome3/GnomeShell, it seems to ignore this move+resize request * unless we sync and sleep here. Sigh. Gnome is retarded. */ //XSync(dpy, 0); //usleep(5000); } else if (resize) { XResizeWindow(dpy, zone, wininfo.w, wininfo.h); } else if (move) { XMoveWindow(dpy, zone, wininfo.x, wininfo.y); } XMapRaised(dpy, zone); } void correct_overflow() { /* If the window is outside the boundaries of the screen, bump it back * or, if possible, move it to the next screen */ if (wininfo.x < viewports[wininfo.curviewport].x) viewport_left(); if (wininfo.x + wininfo.w > viewports[wininfo.curviewport].x + viewports[wininfo.curviewport].w) viewport_right(); /* Fix positioning if we went out of bounds (off the screen) */ if (wininfo.x < 0) { wininfo.x = 0; } if (wininfo.x + wininfo.w > viewports[wininfo.curviewport].x + viewports[wininfo.curviewport].w) wininfo.x = viewports[wininfo.curviewport].x + viewports[wininfo.curviewport].w - wininfo.w; /* XXX: We don't currently understand how to move around if displays are * vertically stacked. */ if (wininfo.y < 0) wininfo.y = 0; if (wininfo.y + wininfo.h > viewports[wininfo.curviewport].y + viewports[wininfo.curviewport].h) wininfo.y = viewports[wininfo.curviewport].h - wininfo.h; } void viewport_right() { int expand_w = 0, expand_h = 0; /* Expand if the current window is the size of the current viewport */ //printf("right %d] %d,%d vs %d,%d\n", wininfo.curviewport, //wininfo.w, wininfo.h, //viewports[wininfo.curviewport].w, viewports[wininfo.curviewport].h); if (wininfo.curviewport == nviewports - 1) return; if (wininfo.w == viewports[wininfo.curviewport].w) expand_w = 1; if (wininfo.h == viewports[wininfo.curviewport].h) { expand_h = 1; } wininfo.curviewport++; if ((expand_w) || wininfo.w > viewports[wininfo.curviewport].w) { wininfo.w = viewports[wininfo.curviewport].w; } if ((expand_h) || wininfo.h > viewports[wininfo.curviewport].h) { wininfo.h = viewports[wininfo.curviewport].h; } wininfo.x = viewports[wininfo.curviewport].x; } void viewport_left() { int expand_w = 0, expand_h = 0; /* Expand if the current window is the size of the current viewport */ //printf("left %d] %d,%d vs %d,%d\n", wininfo.curviewport, //wininfo.w, wininfo.h, //viewports[wininfo.curviewport].w, viewports[wininfo.curviewport].h); if (wininfo.curviewport == 0) return; if (wininfo.w == viewports[wininfo.curviewport].w) expand_w = 1; if (wininfo.h == viewports[wininfo.curviewport].h) { expand_h = 1; } wininfo.curviewport--; if (expand_w || wininfo.w > viewports[wininfo.curviewport].w) { wininfo.w = viewports[wininfo.curviewport].w; } if (expand_h || wininfo.h > viewports[wininfo.curviewport].h) { wininfo.h = viewports[wininfo.curviewport].h; } wininfo.x = viewports[wininfo.curviewport].w - wininfo.w; } void handle_keypress(XKeyEvent *e) { int i; /* If a mouse button is pressed (like, when we're dragging), * then the 'mods' will include values like Button1Mask. * Let's remove those, as they cause breakage */ e->state &= ~(Button1Mask | Button2Mask | Button3Mask | Button4Mask | Button5Mask); /* Ignore LockMask (Numlock, etc) and Mod2Mask (shift, etc) */ e->state &= ~(LockMask | Mod2Mask); if (appstate.recording == record_getkey) { if (handle_recording(e) == HANDLE_STOP) { return; } } if (appstate.playback) { /* Loop over known recordings */ for (i = 0; i < recordings->len; i++) { recording_t *rec = g_ptr_array_index(recordings, i); if (e->keycode == rec->keycode) { int j = 0; for (j = 0; j < rec->commands->len; j++) { handle_commands(g_ptr_array_index(rec->commands, j)); } break; } } appstate.playback = 0; return; } if (appstate.grid_nav) { if (handle_gridnav(e) == HANDLE_STOP) { return; } } /* Loop over known keybindings */ for (i = 0; i < keybindings->len; i++) { keybinding_t *kbt = g_ptr_array_index(keybindings, i); int keycode = kbt->keycode; int mods = kbt->mods; char *commands = kbt->commands; if ((keycode == e->keycode) && (mods == e->state)) { handle_commands(commands); } } } /* void handle_keypress */ handler_info_t handle_recording(XKeyEvent *e) { int i; appstate.recording = record_ing; /* start recording actions */ /* TODO(sissel): support keys with keystrokes like shift+a */ /* check existing recording keycodes if we need to override it */ for (i = 0; i < recordings->len; i++) { recording_t *rec = (recording_t *) g_ptr_array_index(recordings, i); if (rec->keycode == e->keycode) { g_ptr_array_free(rec->commands, TRUE); g_ptr_array_remove_index_fast(recordings, i); i--; /* array removal will shift everything down one to make up for the * loss we'll need to redo this index */ } } //printf("Recording as keycode:%d\n", e->keycode); active_recording->keycode = e->keycode; return HANDLE_CONTINUE; } handler_info_t handle_gridnav(XKeyEvent *e) { int index = 0; if (e->state & 0x2000) { /* ISO Level3 Shift */ index += 2; } if (e->state & ShiftMask) { index += 1; } KeySym sym = XkbKeycodeToKeysym(dpy, e->keycode, 0, index); char *key = XKeysymToString(sym); if (sym == XK_Escape) { cmd_grid_nav("off"); update(); return HANDLE_STOP; } if (!(strlen(key) == 1 && isalpha(*key))) { return HANDLE_CONTINUE; } int val = tolower(*key) - 'a'; if (val < 0) { return HANDLE_CONTINUE; } /* TODO(sissel): Only GRID_LABEL_AA supported right now */ if (appstate.grid_nav_state == GRID_NAV_COL) { if (val >= wininfo.grid_cols) { return HANDLE_CONTINUE; /* Invalid key in this grid, pass */ } appstate.grid_nav_state = GRID_NAV_ROW; appstate.grid_nav_col = val; appstate.need_draw = 1; update(); save_history_point(); /* We have a full set of coordinates now; select that grid position */ cell_select(appstate.grid_nav_col, appstate.grid_nav_row); update(); save_history_point(); appstate.grid_nav_row = -1; appstate.grid_nav_col = -1; } else if (appstate.grid_nav_state == GRID_NAV_ROW) { if (val >= wininfo.grid_rows) { return HANDLE_CONTINUE; /* Invalid key in this grid, pass */ } appstate.grid_nav_row = val; appstate.grid_nav_state = GRID_NAV_COL; appstate.need_draw = 1; update(); save_history_point(); } return HANDLE_STOP; } void handle_commands(char *commands) { char *cmdcopy; char *tokctx, *tok, *strptr; //printf("Commands; %s\n", commands); cmdcopy = strdup(commands); strptr = cmdcopy; while ((tok = strtok_r(strptr, ",", &tokctx)) != NULL) { int i; int found = 0; strptr = NULL; /* Ignore leading whitespace */ while (isspace(*tok)) tok++; /* Record this command (if the command is not 'record') */ if (appstate.recording == record_ing && strncmp(tok, "record", 6)) { //printf("Record: %s\n", tok); g_ptr_array_add(active_recording->commands, (gpointer) strdup(tok)); } for (i = 0; dispatch[i].command; i++) { /* XXX: This approach means we can't have one command be a subset of * another. For example, 'grid' and 'grid-foo' will fail because when you * use 'grid-foo' it'll match 'grid' first. * This hasn't been a problem yet... */ /* If this command starts with a dispatch function, call it */ size_t cmdlen = strcspn(tok, " \t"); if (!strncmp(tok, dispatch[i].command, cmdlen)) { /* tok + len + 1 is * "command arg1 arg2" * ^^^^^^^^^ <-- this */ char *args = tok + cmdlen; if (*args == '\0') args = ""; else args++; found = 1; dispatch[i].func(args); } } if (!found) fprintf(stderr, "No such command: '%s'\n", tok); } if (ISACTIVE) { if (ISDRAGGING) cmd_warp(NULL); update(); save_history_point(); } free(cmdcopy); } void save_history_point() { /* If the history is full, drop the oldest entry */ while (wininfo_history_cursor >= WININFO_MAXHIST) { int i; for (i = 1; i < wininfo_history_cursor; i++) { memcpy(&(wininfo_history[i - 1]), &(wininfo_history[i]), sizeof(wininfo_t)); } wininfo_history_cursor--; } memcpy(&(wininfo_history[wininfo_history_cursor]), &(wininfo), sizeof(wininfo)); wininfo_history_cursor++; } void restore_history_point(int moves_ago) { wininfo_history_cursor -= moves_ago + 1; if (wininfo_history_cursor < 0) { wininfo_history_cursor = 0; } wininfo_t *previous = &(wininfo_history[wininfo_history_cursor]); memcpy(&wininfo, previous, sizeof(wininfo)); appstate.need_draw = 1; appstate.need_moveresize = 1; } /* Sort viewports, left to right. * This may perform undesirably for vertically-stacked viewports or * for other odd configurations */ int viewport_sort(const void *a, const void *b) { viewport_t *va = (viewport_t *)a; viewport_t *vb = (viewport_t *)b; return va->x - vb->x; } void query_screens() { int dummyint; if (XineramaQueryExtension(dpy, &dummyint, &dummyint) && XineramaIsActive(dpy)) { xinerama = True; query_screen_xinerama(); } else { /* No xinerama */ query_screen_normal(); } qsort(viewports, nviewports, sizeof(viewport_t), viewport_sort); } void query_screen_xinerama() { int i; XineramaScreenInfo *screeninfo; screeninfo = XineramaQueryScreens(dpy, &nviewports); viewports = calloc(nviewports, sizeof(viewport_t)); for (i = 0; i < nviewports; i++) { viewports[i].x = screeninfo[i].x_org; viewports[i].y = screeninfo[i].y_org; viewports[i].w = screeninfo[i].width; viewports[i].h = screeninfo[i].height; viewports[i].screen_num = 0; viewports[i].screen = ScreenOfDisplay(dpy, 0); viewports[i].root = DefaultRootWindow(dpy); } XFree(screeninfo); } void query_screen_normal() { int i; Screen *s; nviewports = ScreenCount(dpy); viewports = calloc(nviewports, sizeof(viewport_t)); for (i = 0; i < nviewports; i++) { s = ScreenOfDisplay(dpy, i); viewports[i].x = 0; viewports[i].y = 0; viewports[i].w = s->width; viewports[i].h = s->height; viewports[i].root = RootWindowOfScreen(s); viewports[i].screen_num = i; viewports[i].screen = s; } } int query_current_screen() { int i; if (xinerama) { return query_current_screen_xinerama(); } else { return query_current_screen_normal(); } } int query_current_screen_xinerama() { int i = 0, dummyint; unsigned int dummyuint; int x, y; Window dummywin; Window root = viewports[0].root; XQueryPointer(dpy, root, &dummywin, &dummywin, &x, &y, &dummyint, &dummyint, &dummyuint); /* Figure which display the cursor is on */ for (i = 0; i < nviewports; i++) { if (pointinrect(x, y, viewports[i].x, viewports[i].y, viewports[i].w, viewports[i].h)) { return i; } } return -1; } int query_current_screen_normal() { int i = 0, dummyint; unsigned int dummyuint; int x, y; Window dummywin; Window root = viewports[0].root; /* Query each Screen's root window to ask if the pointer is in it. * I don't know of any other better way to ask what Screen is * active (where is the cursor) */ for (i = 0; i < nviewports; i++) { if (!XQueryPointer(dpy, viewports[i].root, &dummywin, &dummywin, &x, &y, &dummyint, &dummyint, &dummyuint)) continue; if (pointinrect(x, y, viewports[i].x, viewports[i].y, viewports[i].w, viewports[i].h)) { return i; } } return -1; } int pointinrect(int px, int py, int rx, int ry, int rw, int rh) { return (px >= rx) && (px <= rx + rw) && (py >= ry) && (py <= ry + rh); } void sigchld(int sig) { int status; while (waitpid(-1, &status, WNOHANG) > 0) { /* Do nothing, but we needed to waitpid() to collect dead children */ } } void sighup(int sig) { restart(); } void restart() { execvp(g_argv[0], g_argv); } void recordings_save(const char *filename) { FILE *output = NULL; int i = 0; output = fopen(filename, "w"); if (output == NULL) { fprintf(stderr, "Failure opening '%s' for write: %s\n", filename, strerror(errno)); return; /* Should we exit instead? */ } for (i = 0; i < recordings->len; i++) { int j; recording_t *rec = g_ptr_array_index(recordings, i); if (rec->commands->len == 0) continue; fprintf(output, "%d ", rec->keycode); for (j = 0; j < rec->commands->len; j++) { fprintf(output, "%s%s", (char *) g_ptr_array_index(rec->commands, j), (j + 1 < rec->commands->len ? ", " : "")); } fprintf(output, "\n"); } fclose(output); } void parse_recordings(const char *filename) { FILE *fp = fopen(filename, "r"); if (fp == NULL) return; static const int bufsize = 8192; char line[bufsize]; /* fopen succeeded */ while (fgets(line, bufsize, fp) != NULL) { /* Kill the newline */ *(line + strlen(line) - 1) = '\0'; int keycode = 0; char *command = NULL; keycode = atoi(line); command = line + strcspn(line, " \t"); //printf("found recording: %d => %s\n", keycode, command); recording_t *rec = NULL; rec = calloc(sizeof(recording_t), 1); rec->keycode = keycode; rec->commands = g_ptr_array_new(); g_ptr_array_add(rec->commands, (gpointer) strdup(command)); g_ptr_array_add(recordings, (gpointer) rec); } fclose(fp); } void openpixel(Display *dpy, Window zone, mouseinfo_t *mouseinfo) { XRectangle rect; if (mouseinfo->x == -1 && mouseinfo->y == -1) { return; } rect.x = mouseinfo->x; rect.y = mouseinfo->y; rect.width = 1; rect.height = 1; XShapeCombineRectangles(dpy, zone, ShapeBounding, 0, 0, &rect, 1, ShapeSubtract, 0); } /* void openpixel */ void closepixel(Display *dpy, Window zone, mouseinfo_t *mouseinfo) { XRectangle rect; if (mouseinfo->x == -1 && mouseinfo->y == -1) { return; } rect.x = mouseinfo->x; rect.y = mouseinfo->y; rect.width = 1; rect.height = 1; XShapeCombineRectangles(dpy, zone, ShapeBounding, 0, 0, &rect, 1, ShapeUnion, 0); } /* void closepixel */ int main(int argc, char **argv) { char *pcDisplay; int ret; const char *prog = argv[0]; g_argv = argv; if ((pcDisplay = getenv("DISPLAY")) == NULL) { fprintf(stderr, "Error: DISPLAY environment variable not set\n"); return EXIT_SUCCESS; } if ((dpy = XOpenDisplay(pcDisplay)) == NULL) { fprintf(stderr, "Error: Can't open display: %s\n", pcDisplay); return EXIT_SUCCESS; } if (argc > 1 && (!strcmp(argv[1], "version") || !strcmp(argv[1], "-v") || !strcmp(argv[1], "--version"))) { printf("keynav %s\n", KEYNAV_VERSION); return EXIT_SUCCESS; } signal(SIGCHLD, sigchld); signal(SIGHUP, sighup); signal(SIGUSR1, sighup); xdo = xdo_new_with_opened_display(dpy, pcDisplay, False); parse_config(); query_screens(); if (argc == 2) { handle_commands(argv[1]); } else if (argc > 2) { fprintf(stderr, "Usage: %s [command string]\n", prog); fprintf(stderr, "Did you quote your command string?\n"); fprintf(stderr, " Example: %s 'loadconfig mykeynavrc,daemonize'\n", prog); return EXIT_FAILURE; } /* Sync with the X server. * This ensure we errors about XGrabKey and other failures * before we try to daemonize */ XSync(dpy, 0); if (daemonize) { printf("Daemonizing now...\n"); daemon(0, 0); is_daemon = True; } while (1) { XEvent e; XNextEvent(dpy, &e); switch (e.type) { case KeyPress: handle_keypress((XKeyEvent *)&e); break; /* MapNotify means the keynav window is now visible */ case MapNotify: update(); break; // Configure events mean the window was changed (size, property, etc) case ConfigureNotify: update(); break; case Expose: XCopyArea(dpy, canvas, zone, canvas_gc, e.xexpose.x, e.xexpose.y, e.xexpose.width, e.xexpose.height, e.xexpose.x, e.xexpose.y); break; case MotionNotify: if (mouseinfo.x != -1 && mouseinfo.y != -1) { closepixel(dpy, zone, &mouseinfo); } mouseinfo.x = e.xmotion.x; mouseinfo.y = e.xmotion.y; openpixel(dpy, zone, &mouseinfo); break; // Ignorable events. case GraphicsExpose: case NoExpose: case LeaveNotify: // Mouse left the window case KeyRelease: // key was released case DestroyNotify: // window was destroyed case UnmapNotify: // window was unmapped (hidden) case MappingNotify: // when keyboard mapping changes break; default: printf("Unexpected X11 event: %d\n", e.type); break; } } xdo_free(xdo); } /* int main */ keynav-master/keynav.pod000066400000000000000000000263531326653421100156610ustar00rootroot00000000000000=pod =head1 NAME keynav - keyboard navigation tool =head1 SYNOPSIS B [optional-startup-commands] =head1 DESCRIPTION B is a utility for generally operating your mouse with your keyboard. The main usage is to divide the screen into sections, selecting them until you end up at the point where you want to move the mouse, or click, etc. You can pass any valid keynav commands to keynav from the command-line. Make sure you quote things properly. For example, to load another config file or two: keynav "loadconfig ~/myconfigs/keynavrc,loadconfig ~/myconfig/anotherkeynavrc" Another example: daemonize on startup: keynav daemonize =head1 CONFIGURATION keynav is configured by default from a config file in your home directory "~/.keynavrc" The default configuration can be found in the L section. '#' will delimit comments. The configuration consists mostly of binding keys to keynav commands. The following to commands must appear on lines by themselves, not as key bindings. =over =item B This will make keynav background itself after parsing the config file and setting up keybindings. If there are errors during keybinding, then keynav will not background and will exit with failure. =item B This wil clear all existing keybindings. This is useful if, for example, you do not want any of the default keybindings that come with keynav. =back The rest of the configuration has this format keybinding keynav-command[,keynav-command,...] You can have multiple commands for a single keybinding. The way to start keynav's navigation is to use the 'start' command. Additionally, any keys with 'start' as a command will be grabbed as global hotkeys. For example, this configuration: ctrl+semicolon start space click 1 This will make keynav grab the I keybinding globally, but it will not grab I. The I keybinding will only be active while keynav is active (after you press a key sequence that invokes B. If you aren't sure what the name of your key is, you can run xev(1) and press each key you want to learn about while the xev window has focus. The output will include the keysym name (like Shift_L, or Return, etc) Moving on, here are all the keynav commands you can use: =head2 CUTS AND MOVES Movements are bounded by screen edges. See L for edge and multiple screen handling. =over =item B I<[value]> This will cut the keynav window upwards. We will shrink from the bottom upwards. The default cut value, if not specified, is 0.5 See L for what this value means. =item B I<[value]> This will cut the keynav window downwards. We will shrink from the top down. The default cut value, if not specified, is 0.5 See L for what this value means. =item B I<[value]> This will cut the keynav window leftwards. We will shrink from the right to the left. The default cut value, if not specified, is 0.5 See L for what this value means. =item B I<[value]> This will cut the keynav window rightwards. We will shrink from the left to the right. The default cut value, if not specified, is 0.5 See L for what this value means. =item B I<[value]> This will move the window up. The default value, if not specified, is 1. See L for what this value means. =item B I<[value]> This will move the window down. The default value, if not specified, is 1. See L for what this value means. =item B I<[value]> This will move the window left. The default value, if not specified, is 1. See L for what this value means. =item B I<[value]> This will move the window right. The default value, if not specified, is 1. See L for what this value means. =item B I I This command centers the keynav window on the mouse position and sets the window size to the given width and height (in pixels). =item B This command makes the keynav window fit the current application window. This is useful if you have your windows in a tiled arrangement. =back =head2 GRID COMMANDS =over =item B I<[COLUMNSxROWS]> The default 'grid' is 2x2. If you want more grid cells you can choose any number of columns and rows. =item B I<[on OR off OR toggle]> Grid navigation is off by default. When turned on, every grid cell will have a two-letter label. To select a single cell, you simply type the two letters. The 'toggle' value will toggle grid-nav. =item B I<[value]> Cell selection is similar to grid-nav, but less visual. You would bind keys to select specific cells. With cell-select, there are no labels. The value can be one of two formats. First, the COLUMNxROW syntax. 'cell-select 3x1' will select column 3, row 1. The second format is simply a number. The number represents the counting of the cell, starting at the top left and working right. A visual might help. With 'grid 3x3' the following is used with cell-select: +----------------------------------------------+ | | | | | 1x1 | 2x1 | 3x1 | | 1 | 2 | 3 | | | | | |--------------+---------------+---------------| | | | | | 1x2 | 2x2 | 3x2 | | 4 | 5 | 6 | | | | | |--------------+---------------+---------------| | | | | | 1x3 | 2x3 | 3x3 | | 7 | 8 | 9 | | | | | |----------------------------------------------| =back =head2 MOUSE COMMANDS =over =item B This command moves the mouse pointer to the center of the keynav window. =item B I<[button]> This command will send a mouse click. It does not move the mouse, so the click will go wherever the mouse is positioned. The I