Index: src/actions.c =================================================================== RCS file: /cvsroot/ratpoison/ratpoison/src/actions.c,v retrieving revision 1.125 diff -u -b -u -r1.125 actions.c --- src/actions.c 6 Jul 2002 21:28:05 -0000 1.125 +++ src/actions.c 26 Aug 2002 20:49:32 -0000 @@ -31,7 +31,7 @@ static int key_actions_last; static int key_actions_table_size; -static user_command user_commands[] = +user_command user_commands[] = { /address@hidden (tag required for genrpbindings) */ {"abort", cmd_abort, arg_VOID}, {"banish", cmd_banish, arg_VOID}, @@ -723,7 +723,7 @@ rp_window *w; if (data == NULL) - str = get_input (MESSAGE_PROMPT_SWITCH_TO_WINDOW); + str = get_input (MESSAGE_PROMPT_SWITCH_TO_WINDOW, COMPLETE_WINDOW); else str = xstrdup ((char *) data); @@ -775,7 +775,7 @@ if (current_window() == NULL) return NULL; if (data == NULL) - winname = get_input (MESSAGE_PROMPT_NEW_WINDOW_NAME); + winname = get_input (MESSAGE_PROMPT_NEW_WINDOW_NAME, COMPLETE_NONE); else winname = xstrdup ((char *) data); @@ -927,9 +927,9 @@ char *input; if (data == NULL) - input = get_input (MESSAGE_PROMPT_COMMAND); + input = get_input (MESSAGE_PROMPT_COMMAND, COMPLETE_COMMAND); else - input = get_more_input (MESSAGE_PROMPT_COMMAND, data); + input = get_more_input (MESSAGE_PROMPT_COMMAND, data, COMPLETE_COMMAND); /* User aborted. */ if (input == NULL) @@ -952,7 +952,7 @@ char *cmd; if (data == NULL) - cmd = get_input (MESSAGE_PROMPT_SHELL_COMMAND); + cmd = get_input (MESSAGE_PROMPT_SHELL_COMMAND, COMPLETE_EXECUTABLE); else cmd = xstrdup ((char *) data); @@ -1006,7 +1006,7 @@ char *prog; if (data == NULL) - prog = get_input (MESSAGE_PROMPT_SWITCH_WM); + prog = get_input (MESSAGE_PROMPT_SWITCH_WM, COMPLETE_NONE); else prog = xstrdup ((char *) data); Index: src/actions.h =================================================================== RCS file: /cvsroot/ratpoison/ratpoison/src/actions.h,v retrieving revision 1.49 diff -u -b -u -r1.49 actions.h --- src/actions.h 24 Mar 2002 06:05:03 -0000 1.49 +++ src/actions.h 26 Aug 2002 20:49:33 -0000 @@ -37,6 +37,8 @@ int argtype; }; +extern user_command user_commands[]; + void spawn(void *data); char * command (int interactive, char *data); Index: src/data.h =================================================================== RCS file: /cvsroot/ratpoison/ratpoison/src/data.h,v retrieving revision 1.37 diff -u -b -u -r1.37 data.h --- src/data.h 13 Mar 2002 08:23:30 -0000 1.37 +++ src/data.h 26 Aug 2002 20:49:33 -0000 @@ -94,7 +94,7 @@ { GC normal_gc; XWindowAttributes root_attr; - Window root, bar_window, key_window, input_window, frame_window, help_window; + Window root, bar_window, key_window, input_window, frame_window, help_window, completions_window; int bar_is_raised; int screen_num; /* Our screen number as dictated my X */ Colormap def_cmap; Index: src/input.c =================================================================== RCS file: /cvsroot/ratpoison/ratpoison/src/input.c,v retrieving revision 1.26 diff -u -b -u -r1.26 input.c --- src/input.c 15 Jan 2002 18:50:42 -0000 1.26 +++ src/input.c 26 Aug 2002 20:49:33 -0000 @@ -22,6 +22,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -32,6 +36,19 @@ static char *input_history[INPUT_MAX_HISTORY]; static int input_num_history_entries = 0; +/* Non-zero if the completions window is visible. */ +static int completions_visible = 0; + +static char **find_executable_completions (char *test, int *num_matches); +static char **find_command_completions (char *test, int *num_matches); +static char **find_window_completions (char *test, int *num_matches); +static void free_completion_matches (char **matches, int len); +static char *longest_common_string (char **matches, int len); +static int compare (const void *p1, const void *p2); +static void show_completion_matches (char **matches, int len); +static void hide_completion_matches (void); + + /* Convert an X11 modifier mask to the rp modifier mask equivalent, as best it can (the X server may not have a hyper key defined, for instance). */ @@ -313,13 +330,13 @@ } char * -get_input (char *prompt) +get_input (char *prompt, int completion_type) { - return get_more_input (prompt, ""); + return get_more_input (prompt, "", completion_type); } char * -get_more_input (char *prompt, char *preinput) +get_more_input (char *prompt, char *preinput, int completion_type) { /* Emacs 21 uses a 513 byte string to store the keysym name. */ char keysym_buf[513]; @@ -366,6 +383,44 @@ if (cur_len > 0) cur_len--; update_input_window(s, prompt, str, cur_len); } + else if (ch == XK_Tab) + { + char *test, **matches = NULL; + int len = 0; + + hide_completion_matches (); + + test = xstrdup (str); + test[cur_len] = '\0'; + + if (completion_type == COMPLETE_EXECUTABLE) + matches = find_executable_completions (test, &len); + else if (completion_type == COMPLETE_COMMAND) + matches = find_command_completions (test, &len); + else if (completion_type == COMPLETE_WINDOW) + matches = find_window_completions (test, &len); + + if (len > 0) + { + free (str); + + if (len == 1) + str = xstrdup (matches[0]); + else + { + str = longest_common_string (matches, len); + show_completion_matches (matches, len); + } + + allocated_len = strlen (str) + 1; + cur_len = allocated_len - 1; + + free_completion_matches (matches, len); + update_input_window (s, prompt, str, cur_len); + } + + free (test); + } else if (ch == INPUT_PREV_HISTORY_KEY && modifier == INPUT_PREV_HISTORY_MODIFIER) { @@ -383,6 +438,7 @@ allocated_len = strlen (str) + 1; cur_len = allocated_len - 1; + hide_completion_matches (); update_input_window (s, prompt, str, cur_len); } } @@ -403,6 +459,7 @@ allocated_len = strlen (str) + 1; cur_len = allocated_len - 1; + hide_completion_matches (); update_input_window (s, prompt, str, cur_len); } } @@ -410,6 +467,7 @@ { /* User aborted. */ free (str); + hide_completion_matches (); XSetInputFocus (dpy, fwin, RevertToPointerRoot, CurrentTime); XUnmapWindow (dpy, s->input_window); return NULL; @@ -451,7 +509,392 @@ input_history[input_num_history_entries] = xstrdup (str); input_num_history_entries++; + hide_completion_matches (); + XSetInputFocus (dpy, fwin, RevertToPointerRoot, CurrentTime); XUnmapWindow (dpy, s->input_window); return str; } + + +/* Return a list of all executable files in $PATH starting with TEST. + Second argument NUM_MATCHES is set to the length of the list. */ + +static char ** +find_executable_completions (char *test, int *num_matches) +{ + char *path, *dirname, **list; + int len, matches, max_size; + struct dirent *item; + DIR *dir; + + path = xstrdup (getenv ("PATH")); + len = strlen (test); + matches = 0; + max_size = 1; + + list = xmalloc (max_size * sizeof (char *)); + + dirname = strtok (path, ":\0"); + while (dirname) + { + if (!(dir = opendir (dirname))) + { + dirname = strtok (NULL, ":\0"); + continue; + } + + while ((item = readdir (dir))) + { + struct stat sb; + char *tmp; + int ret; + + if (strcmp (".", item->d_name) == 0 + || strcmp ("..", item->d_name) == 0 + || strncmp (test, item->d_name, len) != 0) + continue; + + tmp = xsprintf ("%s/%s", dirname, item->d_name); + ret = stat (tmp, &sb); + free (tmp); + + /* If file isn't executable, ignore it. */ + if ((ret < 0) && !S_ISREG (sb.st_mode) + && !(sb.st_mode & (S_IXOTH | S_IXGRP | S_IXUSR))) + continue; + + if (matches >= max_size) + { + max_size *= 2; + list = xrealloc (list, max_size * sizeof (char *)); + } + + list[matches] = xstrdup (item->d_name); + matches++; + } + + closedir (dir); + dirname = strtok (NULL, ":\0"); + } + + free (path); + + *num_matches = matches; + return list; +} + + +/* Return a list of all ratpoison commands starting with TEST. Second + argument NUM_MATCHES is set to the length of the list. */ + +static char ** +find_command_completions (char *test, int *num_matches) +{ + int len, matches, max_size; + user_command *uc; + char **list; + + len = strlen (test); + max_size = 1; + matches = 0; + + list = xmalloc (max_size * sizeof (char *)); + + for (uc = user_commands; uc->name; uc++) + { + if (strncmp (test, uc->name, len) != 0) + continue; + + if (matches >= max_size) + { + max_size *= 2; + list = xrealloc (list, max_size * sizeof (char *)); + } + + list[matches] = xstrdup (uc->name); + matches++; + } + + *num_matches = matches; + return list; +} + + +/* Return a list of all managed and mapped windows where the title + starts with TEST. Second argument NUM_MATCHES is set to the length + of the list. */ + +static char ** +find_window_completions (char *test, int *num_matches) +{ + int len, matches, max_size; + rp_window *cur; + char **list; + + len = strlen (test); + max_size = 1; + matches = 0; + + list = xmalloc (max_size * sizeof (char *)); + + for (cur = rp_mapped_window_sentinel->next; + cur != rp_mapped_window_sentinel; + cur = cur->next) + { + char *name; + + if (!(name = window_name (cur))) + continue; + + if (strncmp (test, name, len) != 0) + continue; + + if (matches >= max_size) + { + max_size *= 2; + list = xrealloc (list, max_size * sizeof (char *)); + } + + list[matches] = xstrdup (name); + matches++; + } + + *num_matches = matches; + return list; +} + + +/* Free storage used by the above functions. */ + +static void +free_completion_matches (char **matches, int len) +{ + int i; + + if (!matches) + return; + + for (i = 0; i < len; i++) + free (matches[i]); + + free (matches); +} + + +/* Find longest string common to all completion matches. */ + +static char * +longest_common_string (char **matches, int len) +{ + int i, j, c, longest = -1; + char *str; + + if (len < 1) + return NULL; + + if (len == 1) + return xstrdup (matches[0]); + + for (i = 0, j = 1; j < len; j++, i++) + { + for (c = 0; matches[i][c] != '\0'; c++) + { + if (matches[i][c] != matches[j][c]) + break; + } + + if (longest == -1 || longest > c) + longest = c; + } + + str = xstrdup (matches[0]); + + if (longest < strlen (matches[0])) + str[longest] = '\0'; + + return str; +} + + +/* Compare two strings for qsort. */ + +static int +compare (const void *p1, const void *p2) +{ + const char *v1, *v2; + + v1 = *(char **) p1; + v2 = *(char **) p2; + + return strcmp (v1, v2); +} + + +/* Display the window showing possible completions. This function is + really kludgy, but it was really hot outside when I wrote it and I + don't have air conditioning. This was the best I could come up with + under those circumstances. */ + +static void +show_completion_matches (char **matches, int len) +{ + screen_info *s = current_screen (); + int i, lines, item_width, line_height, tmp; + int cols, max_cols, rows, max_rows; + int x, y, w, h, bar_h; + + if (!completions_visible) + XMapWindow (dpy, s->completions_window); + + XRaiseWindow (dpy, s->completions_window); + XClearWindow (dpy, s->completions_window); + + /* Find the width of the longest item. */ + item_width = 0; + tmp = 0; + + for (i = 0; i < len; i++) + { + int n = strlen (matches[i]); + + if (n > item_width) + { + item_width = n; + tmp = i; + } + } + + item_width = XTextWidth (defaults.font, matches[tmp], strlen (matches[tmp])) + + XTextWidth (defaults.font, "X", 1) + defaults.bar_x_padding; + + /* Find out how many cols and rows we need. */ + line_height = defaults.bar_y_padding + defaults.font->max_bounds.ascent; + bar_h = (FONT_HEIGHT (defaults.font) + (defaults.bar_y_padding * 2)); + + max_cols = s->root_attr.width / item_width; + + if (defaults.bar_location == CenterGravity + || defaults.bar_location == EastGravity + || defaults.bar_location == WestGravity) + max_rows = (s->root_attr.height - bar_y (s) + bar_h) / line_height; + else + max_rows = (s->root_attr.height - bar_h) / line_height; + + cols = (len < 3) ? 1 : 3; + rows = 1; + + while (1) + { + /* If matches won't fit, show as many as possible. */ + if ((cols > max_cols) && (rows > max_rows)) + { + cols = max_cols; + rows = max_rows; + break; + } + + if (cols > max_cols) + cols--; + + if (len > cols) + { + rows = len / cols; + + if ((len % cols) != 0) + rows++; + } + + if ((cols <= max_cols) && (rows <= max_rows)) + break; + + if (rows > max_rows) + cols++; + } + + w = item_width * cols; + h = (rows * line_height) + (defaults.bar_y_padding * 2) + + defaults.font->max_bounds.descent; + + /* Where do we put the window. */ + x = y = 0; + switch (defaults.bar_location) + { + case CenterGravity: + x = (s->root_attr.width / 2) - (w / 2); + y = (bar_y (s) + bar_h) + (defaults.bar_border_width * 3); + break; + case EastGravity: + x = s->root_attr.width - w; + y = (bar_y (s) + bar_h) + (defaults.bar_border_width * 3); + break; + case NorthEastGravity: + x = s->root_attr.width - w; + y = bar_h + (defaults.bar_border_width * 3); + break; + case NorthGravity: + x = (s->root_attr.width / 2) - (w / 2); + y = bar_h + (defaults.bar_border_width * 3); + break; + case NorthWestGravity: + x = 0; + y = bar_h + (defaults.bar_border_width * 3); + break; + case SouthEastGravity: + x = s->root_attr.width - w; + y = (s->root_attr.height - bar_h) - h - (defaults.bar_border_width * 5); + break; + case SouthGravity: + x = (s->root_attr.width / 2) - (w / 2); + y = (s->root_attr.height - bar_h) - h - (defaults.bar_border_width * 5); + break; + case SouthWestGravity: + x = 0; + y = (s->root_attr.height - bar_h) - h - (defaults.bar_border_width * 5); + break; + case WestGravity: + x = 0; + y = (bar_y (s) + bar_h) + (defaults.bar_border_width * 3); + break; + } + + XMoveResizeWindow (dpy, s->completions_window, x, y, w, h); + + qsort (matches, len, sizeof (char **), compare); + + lines = 1; + tmp = 0; + for (i = 0; i < len; i++) + { + if (i == (rows * cols)) + break; + + XDrawString (dpy, s->completions_window, s->normal_gc, + (tmp == 0) ? defaults.bar_x_padding : tmp * item_width, + lines * line_height, matches[i], strlen (matches[i])); + + tmp++; + if (tmp == cols) + { + lines++; + tmp = 0; + } + } + + completions_visible = 1; +} + + +/* Hide the completion window. */ + +static void +hide_completion_matches (void) +{ + screen_info *s = current_screen (); + + if (!completions_visible) + return; + + XUnmapWindow (dpy, s->completions_window); + completions_visible = 0; +} Index: src/input.h =================================================================== RCS file: /cvsroot/ratpoison/ratpoison/src/input.h,v retrieving revision 1.11 diff -u -b -u -r1.11 input.h --- src/input.h 18 Oct 2001 07:51:59 -0000 1.11 +++ src/input.h 26 Aug 2002 20:49:33 -0000 @@ -22,10 +22,18 @@ #ifndef _RATPOISON_INPUT_H #define _RATPOISON_INPUT_H 1 +enum + { + COMPLETE_EXECUTABLE, + COMPLETE_COMMAND, + COMPLETE_WINDOW, + COMPLETE_NONE + }; + char *keysym_to_string (KeySym keysym, unsigned int modifier); int cook_keycode (XKeyEvent *ev, KeySym *keysym, unsigned int *mod, char *keysym_name, int len, int ignore_bad_mods); -char *get_input (char *prompt); -char *get_more_input (char *prompt, char *preinput); +char *get_input (char *prompt, int completion_type); +char *get_more_input (char *prompt, char *preinput, int completion_type); int read_key (KeySym *keysym, unsigned int *modifiers, char *keysym_name, int len); unsigned int x11_mask_to_rp_mask (unsigned int mask); unsigned int rp_mask_to_x11_mask (unsigned int mask); Index: src/main.c =================================================================== RCS file: /cvsroot/ratpoison/ratpoison/src/main.c,v retrieving revision 1.59 diff -u -b -u -r1.59 main.c --- src/main.c 28 Jul 2002 23:07:04 -0000 1.59 +++ src/main.c 26 Aug 2002 20:49:33 -0000 @@ -660,6 +660,11 @@ s->help_window = XCreateSimpleWindow (dpy, s->root, 0, 0, s->root_attr.width, s->root_attr.height, 0, s->fg_color, s->bg_color); XSelectInput (dpy, s->help_window, KeyPressMask); + + /* Create the completions list window. */ + s->completions_window = XCreateSimpleWindow (dpy, s->root, 0, 0, 1, 1, + defaults.bar_border_width, + s->fg_color, s->bg_color); XSync (dpy, 0); } Index: src/ratpoison.h =================================================================== RCS file: /cvsroot/ratpoison/ratpoison/src/ratpoison.h,v retrieving revision 1.14 diff -u -b -u -r1.14 ratpoison.h --- src/ratpoison.h 21 Dec 2001 11:58:56 -0000 1.14 +++ src/ratpoison.h 26 Aug 2002 20:49:33 -0000 @@ -48,6 +48,7 @@ #include "conf.h" +#include "actions.h" #include "data.h" #include "manage.h" #include "list.h"