2 * awesome-menu.c - menu window for awesome
4 * Copyright © 2008 Julien Danjou <julien@danjou.info>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 #define CHUNK_SIZE 4096
34 #include <sys/types.h>
40 #include <xcb/xcb_atom.h>
41 #include <xcb/xcb_keysyms.h>
43 /* XKeysymToString() */
46 #include "common/swindow.h"
47 #include "common/util.h"
48 #include "common/version.h"
49 #include "common/configopts.h"
50 #include "common/xutil.h"
51 #include "common/xscreen.c"
53 #define PROGNAME "awesome-menu"
55 #define CLEANMASK(mask) (mask & ~(globalconf.numlockmask | XCB_MOD_MASK_LOCK))
57 /** awesome-menu run status */
60 /** Stop awesome-menu */
62 /** Run awesome-menu */
64 /** Stop awesome-menu and cancel any operation */
68 /** Is awesome-menu running ? */
69 static status_t status
= RUN
;
71 /** Import awesome config file format */
72 extern cfg_opt_t awesome_opts
[];
75 typedef struct item_t item_t
;
76 /** Item_t structure */
81 /** Previous and next elems in item_t list */
83 /** True if the item currently matches */
87 /** Destructor for item structure
88 * \param item item pointer
91 item_delete(item_t
**item
)
93 p_delete(&(*item
)->data
);
97 DO_SLIST(item_t
, item
, item_delete
)
99 /** awesome-run global configuration structure */
102 /** Connection ref */
103 xcb_connection_t
*connection
;
104 /** Default screen number */
108 /** The draw contet */
117 xcb_key_symbols_t
*keysyms
;
119 unsigned int numlockmask
;
120 /** Shiftlock mask */
121 unsigned int shiftlockmask
;
123 unsigned int capslockmask
;
126 /** The text when we asked to complete */
128 /** The text length */
133 item_t
*item_selected
;
134 /** What to do with the result text */
140 static AwesomeMenuConf globalconf
;
142 /** Exit with given exit code
143 * \param exit_code exit code
144 * \return never returns
146 static void __attribute__ ((noreturn
))
147 exit_help(int exit_code
)
149 FILE *outfile
= (exit_code
== EXIT_SUCCESS
) ? stdout
: stderr
;
150 fprintf(outfile
, "Usage: %s [-c config] [-e command] <message>\n",
155 /** Parse configuration file and fill up AwesomeMenuConf
156 * data structures with configuration directives.
157 * \param screen screen number
158 * \param confpatharg configuration file pathname, or NULL if auto
159 * \param menu_title menu title
160 * \param geometry geometry to fill up with supplied information from
162 * \return cfg_parse status
165 config_parse(int screen
, const char *confpatharg
,
166 const char *menu_title
, area_t
*geometry
)
170 cfg_t
*cfg
, *cfg_menu
= NULL
, *cfg_screen
= NULL
,
171 *cfg_styles
, *cfg_menu_styles
= NULL
;
174 confpath
= config_file();
176 confpath
= a_strdup(confpatharg
);
180 switch((ret
= cfg_parse(cfg
, confpath
)))
183 warn("parsing configuration file failed: %s\n", strerror(errno
));
185 case CFG_PARSE_ERROR
:
186 cfg_error(cfg
, "W: awesome: parsing configuration file %s failed.\n", confpath
);
193 if(menu_title
&& !(cfg_menu
= cfg_gettsec(cfg
, "menu", menu_title
)))
194 warn("no definition for menu %s in configuration file: using default\n",
197 /* get global screen section */
198 if(!(cfg_screen
= cfg_getnsec(cfg
, "screen", screen
)))
199 cfg_screen
= cfg_getsec(cfg
, "screen");
203 cfg_menu_styles
= cfg_getsec(cfg_menu
, "styles");
204 if((i
= cfg_getint(cfg_menu
, "x")) != (int) 0xffffffff)
206 if((i
= cfg_getint(cfg_menu
, "y")) != (int) 0xffffffff)
208 if((i
= cfg_getint(cfg_menu
, "width")) > 0)
210 if((i
= cfg_getint(cfg_menu
, "height")) > 0)
211 geometry
->height
= i
;
215 && (cfg_styles
= cfg_getsec(cfg_screen
, "styles")))
217 /* Grab default styles */
218 draw_style_init(globalconf
.connection
, globalconf
.default_screen
,
219 cfg_getsec(cfg_styles
, "normal"),
220 &globalconf
.styles
.normal
, NULL
);
222 draw_style_init(globalconf
.connection
, globalconf
.default_screen
,
223 cfg_getsec(cfg_styles
, "focus"),
224 &globalconf
.styles
.focus
, &globalconf
.styles
.normal
);
227 /* Now grab menu styles if any */
230 draw_style_init(globalconf
.connection
, globalconf
.default_screen
,
231 cfg_getsec(cfg_menu_styles
, "normal"),
232 &globalconf
.styles
.normal
, NULL
);
234 draw_style_init(globalconf
.connection
, globalconf
.default_screen
,
235 cfg_getsec(cfg_menu_styles
, "focus"),
236 &globalconf
.styles
.focus
, &globalconf
.styles
.normal
);
239 if(!globalconf
.styles
.normal
.font
)
240 eprint("no default font available\n");
247 /** Return the last word for a text.
248 * \param text the text to look into
249 * \return a pointer to the last word position in text
252 get_last_word(char *text
)
256 if((last_word
= strrchr(text
, ' ')))
264 /** Fill the completion list for awesome-menu with file list.
265 * \param directory directory to look into
266 * \return always true
269 item_list_fill_file(const char *directory
)
271 char *cwd
, *home
, *user
, *filename
;
274 struct dirent
*dirinfo
;
276 ssize_t len
, lenfile
;
277 struct passwd
*passwd
= NULL
;
280 item_list_wipe(&globalconf
.items
);
283 cwd
= a_strdup("./");
284 else if(a_strlen(directory
) > 1 && directory
[0] == '~')
286 if(directory
[1] == '/')
288 home
= getenv("HOME");
289 asprintf(&cwd
, "%s%s", (home
? home
: ""), directory
+ 1);
293 if(!(file
= strchr(directory
, '/')))
294 file
= directory
+ a_strlen(directory
);
295 len
= (file
- directory
) + 1;
296 user
= p_new(char, len
);
297 a_strncpy(user
, len
, directory
+ 1, (file
- directory
) - 1);
298 if((passwd
= getpwnam(user
)))
300 asprintf(&cwd
, "%s%s", passwd
->pw_dir
, file
);
311 cwd
= a_strdup(directory
);
313 if(!(dir
= opendir(cwd
)))
319 while((dirinfo
= readdir(dir
)))
321 item
= p_new(item_t
, 1);
323 /* + 1 for \0 + 1 for / if directory */
324 len
= a_strlen(directory
) + a_strlen(dirinfo
->d_name
) + 2;
326 item
->data
= p_new(char, len
);
327 if(a_strlen(directory
))
328 a_strcpy(item
->data
, len
, directory
);
329 a_strcat(item
->data
, len
, dirinfo
->d_name
);
331 lenfile
= a_strlen(cwd
) + a_strlen(dirinfo
->d_name
) + 2;
333 filename
= p_new(char, lenfile
);
334 a_strcpy(filename
, lenfile
, cwd
);
335 a_strcat(filename
, lenfile
, dirinfo
->d_name
);
337 if(!stat(filename
, &st
) && S_ISDIR(st
.st_mode
))
338 a_strcat(item
->data
, len
, "/");
342 item_list_push(&globalconf
.items
, item
);
352 complete(bool reverse
)
356 item_t
*(*item_iter
)(item_t
**, item_t
*) = item_list_next_cycle
;
359 item_iter
= item_list_prev_cycle
;
361 if(globalconf
.item_selected
)
362 item
= item_iter(&globalconf
.items
, globalconf
.item_selected
);
364 item
= globalconf
.items
;
366 for(; item
&& loop
; item
= item_iter(&globalconf
.items
, item
))
370 a_strcpy(globalconf
.text_complete
,
371 globalconf
.text_size
- (globalconf
.text_complete
- globalconf
.text
),
373 globalconf
.item_selected
= item
;
376 /* Since loop is 2, it will be 1 at first iter, and then 0 if we
377 * get back before matching an item (i.e. no items match) to the
378 * first elem: so it will break the loop, otherwise it loops for
381 if(item
== globalconf
.items
)
386 /** Compute a match from completion list for word.
387 * \param word the word to match
390 compute_match(const char *word
)
392 ssize_t len
= a_strlen(word
);
395 /* reset the selected item to NULL */
396 globalconf
.item_selected
= NULL
;
400 if(word
[len
- 1] == '/'
401 || word
[len
- 1] == ' ')
402 item_list_fill_file(word
);
404 for(item
= globalconf
.items
; item
; item
= item
->next
)
405 if(!a_strncmp(word
, item
->data
, a_strlen(word
)))
412 if(a_strlen(globalconf
.text
))
413 item_list_fill_file(NULL
);
414 for(item
= globalconf
.items
; item
; item
= item
->next
)
422 /** Redraw the menu. */
427 area_t geometry
= { 0, 0, 0, 0, NULL
, NULL
};
428 bool selected_item_is_drawn
= false;
429 int len
, prompt_len
, x_of_previous_item
;
432 geometry
.width
= globalconf
.sw
->geometry
.width
;
433 geometry
.height
= globalconf
.sw
->geometry
.height
;
435 if(a_strlen(globalconf
.prompt
))
437 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
438 MARGIN
, globalconf
.prompt
, globalconf
.styles
.focus
);
440 len
= MARGIN
* 2 + draw_textwidth(globalconf
.connection
, globalconf
.default_screen
,
441 globalconf
.styles
.focus
.font
, globalconf
.prompt
);
443 geometry
.width
-= len
;
446 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
447 MARGIN
, globalconf
.text
, globalconf
.styles
.normal
);
449 len
= MARGIN
* 2 + MAX(draw_textwidth(globalconf
.connection
, globalconf
.default_screen
,
450 globalconf
.styles
.normal
.font
, globalconf
.text
),
451 geometry
.width
/ 20);
453 geometry
.width
-= len
;
454 prompt_len
= geometry
.x
;
456 for(item
= globalconf
.items
; item
&& geometry
.width
> 0; item
= item
->next
)
459 style
= item
== globalconf
.item_selected
? globalconf
.styles
.focus
: globalconf
.styles
.normal
;
460 len
= MARGIN
+ draw_textwidth(globalconf
.connection
, globalconf
.default_screen
,
461 style
.font
, item
->data
);
462 if(item
== globalconf
.item_selected
)
464 if(len
> geometry
.width
)
467 selected_item_is_drawn
= true;
469 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
470 MARGIN
/ 2, item
->data
, style
);
472 geometry
.width
-= len
;
475 /* we have an item selected but not drawn, so redraw in the other side */
476 if(globalconf
.item_selected
&& !selected_item_is_drawn
)
478 geometry
.x
= globalconf
.sw
->geometry
.width
;
480 for(item
= globalconf
.item_selected
; item
; item
= item_list_prev(&globalconf
.items
, item
))
483 style
= item
== globalconf
.item_selected
? globalconf
.styles
.focus
: globalconf
.styles
.normal
;
484 x_of_previous_item
= geometry
.x
;
485 geometry
.width
= MARGIN
+ draw_textwidth(globalconf
.connection
, globalconf
.default_screen
,
486 style
.font
, item
->data
);
487 geometry
.x
-= geometry
.width
;
489 if(geometry
.x
< prompt_len
)
492 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
493 MARGIN
/ 2, item
->data
, style
);
498 geometry
.x
= prompt_len
;
499 geometry
.width
= x_of_previous_item
- prompt_len
;
500 draw_rectangle(globalconf
.ctx
, geometry
, 1.0, true, globalconf
.styles
.normal
.bg
);
503 else if(geometry
.width
)
504 draw_rectangle(globalconf
.ctx
, geometry
, 1.0, true, globalconf
.styles
.normal
.bg
);
506 simplewindow_refresh_drawable(globalconf
.sw
, globalconf
.default_screen
);
507 xcb_aux_sync(globalconf
.connection
);
510 /** XCB equivalent of XLookupString which translate the keycode given
511 * by PressEvent to a KeySym and a string
515 key_press_lookup_string(xcb_key_press_event_t
*e
,
516 char **buf
, size_t *buf_len
,
521 /* 'col' (third parameter) is used to get the proper KeySym
522 * according to modifier (XCB doesn't provide an equivalent to
523 * XLookupString()) */
524 k0
= xcb_key_press_lookup_keysym(globalconf
.keysyms
, e
, 0);
525 k1
= xcb_key_press_lookup_keysym(globalconf
.keysyms
, e
, 1);
527 /* The numlock modifier is on and the second KeySym is a keypad
529 if((e
->state
& globalconf
.numlockmask
) && xcb_is_keypad_key(k1
))
531 /* The Shift modifier is on, or if the Lock modifier is on and
532 * is interpreted as ShiftLock, use the first KeySym */
533 if((e
->state
& XCB_MOD_MASK_SHIFT
) ||
534 (e
->state
& XCB_MOD_MASK_LOCK
&& (e
->state
& globalconf
.shiftlockmask
)))
540 /* The Shift and Lock modifers are both off, use the first
542 else if(!(e
->state
& XCB_MOD_MASK_SHIFT
) && !(e
->state
& XCB_MOD_MASK_LOCK
))
545 /* The Shift modifier is off and the Lock modifier is on and is
546 * interpreted as CapsLock */
547 else if(!(e
->state
& XCB_MOD_MASK_SHIFT
) &&
548 (e
->state
& XCB_MOD_MASK_LOCK
&& (e
->state
& globalconf
.capslockmask
)))
549 /* The first Keysym is used but if that KeySym is lowercase
550 * alphabetic, then the corresponding uppercase KeySym is used
554 /* The Shift modifier is on, and the Lock modifier is on and is
555 * interpreted as CapsLock */
556 else if((e
->state
& XCB_MOD_MASK_SHIFT
) &&
557 (e
->state
& XCB_MOD_MASK_LOCK
&& (e
->state
& globalconf
.capslockmask
)))
558 /* The second Keysym is used but if that KeySym is lowercase
559 * alphabetic, then the corresponding uppercase KeySym is used
563 /* The Shift modifer is on, or the Lock modifier is on and is
564 * interpreted as ShiftLock, or both */
565 else if((e
->state
& XCB_MOD_MASK_SHIFT
) ||
566 (e
->state
& XCB_MOD_MASK_LOCK
&& (e
->state
& globalconf
.shiftlockmask
)))
569 if(xcb_is_modifier_key(*ksym
) || xcb_is_function_key(*ksym
) ||
570 xcb_is_pf_key(*ksym
) || xcb_is_cursor_key(*ksym
) ||
571 xcb_is_misc_function_key(*ksym
))
579 asprintf(buf
, "%c", (char) *ksym
);
581 /* TODO: remove this call in favor of XCB */
582 *buf
= a_strdup(XKeysymToString(*ksym
));
584 *buf_len
= a_strlen(*buf
);
587 /** Handle keypress event in awesome-menu.
588 * \param e received XKeyEvent
591 handle_kpress(xcb_key_press_event_t
*e
)
596 size_t text_dst_len
, num
;
598 len
= a_strlen(globalconf
.text
);
600 key_press_lookup_string(e
, &buf
, &num
, &ksym
);
601 /* Got a special key, see x_lookup_string() */
605 if(e
->state
& XCB_MOD_MASK_CONTROL
)
636 while(i
>= 0 && globalconf
.text
[i
] == ' ')
637 globalconf
.text
[i
--] = '\0';
638 while(i
>= 0 && globalconf
.text
[i
] != ' ')
639 globalconf
.text
[i
--] = '\0';
640 compute_match(get_last_word(globalconf
.text
));
646 else if(CLEANMASK(e
->state
) & XCB_MOD_MASK_1
)
663 globalconf
.text_complete
= globalconf
.text
+ a_strlen(globalconf
.text
) + 1;
665 if(num
&& !iscntrl((int) buf
[0]))
667 if(buf
[0] != '/' || globalconf
.text
[len
- 1] != '/')
669 /* Reallocate text string if needed to hold
670 * concatenation of text and buf */
671 if((text_dst_len
= (a_strlen(globalconf
.text
) + num
- 1)) > globalconf
.text_size
)
673 globalconf
.text_size
+= ((int) (text_dst_len
/ globalconf
.text_size
)) * CHUNK_SIZE
;
674 p_realloc(&globalconf
.text
, globalconf
.text_size
);
676 a_strncat(globalconf
.text
, globalconf
.text_size
, buf
, num
);
678 compute_match(get_last_word(globalconf
.text
));
687 globalconf
.text
[--len
] = '\0';
688 compute_match(get_last_word(globalconf
.text
));
692 case XK_ISO_Left_Tab
:
715 getline(char ** buf
, size_t* len
, FILE* in
)
726 p_realloc(buf
, *len
+ 10);
728 for (i
= 0; i
< 10 && !feof(in
); i
++) {
729 (*buf
)[*len
- 10 + i
] = getchar();
730 if ((*buf
)[*len
- 10 + i
] == '\n' ||
731 (*buf
)[*len
- 10 + i
] == '\r') {
732 return (*len
- 10 + i
+ 1);
741 /** Fill the completion by reading on stdin.
742 * \return true if something has been filled up, false otherwise.
745 item_list_fill_stdin(void)
752 bool has_entry
= false;
754 item_list_init(&globalconf
.items
);
756 if((line_len
= getline(&buf
, &len
, stdin
)) != -1)
762 buf
[line_len
- 1] = '\0';
763 newitem
= p_new(item_t
, 1);
764 newitem
->data
= a_strdup(buf
);
765 newitem
->match
= true;
766 item_list_append(&globalconf
.items
, newitem
);
768 while((line_len
= getline(&buf
, &len
, stdin
)) != -1);
776 /** Grab the keyboard.
782 xcb_grab_keyboard_reply_t
*xgb
= NULL
;
783 for(i
= 1000; i
; i
--)
785 if((xgb
= xcb_grab_keyboard_reply(globalconf
.connection
,
786 xcb_grab_keyboard(globalconf
.connection
, true,
787 xcb_aux_get_screen(globalconf
.connection
, globalconf
.default_screen
)->root
,
788 XCB_CURRENT_TIME
, XCB_GRAB_MODE_ASYNC
,
789 XCB_GRAB_MODE_ASYNC
),
798 eprint("cannot grab keyboard");
801 /** Main function of awesome-menu.
802 * \param argc number of elements in argv
803 * \param argv arguments array
804 * \return EXIT_SUCCESS
807 main(int argc
, char **argv
)
809 xcb_generic_event_t
*ev
;
810 int opt
, ret
, screen
= 0;
811 xcb_query_pointer_reply_t
*xqp
= NULL
;
812 char *configfile
= NULL
, *cmd
;
814 const char *shell
= getenv("SHELL");
815 area_t geometry
= { 0, 0, 0, 0, NULL
, NULL
};
817 static struct option long_options
[] =
819 {"help", 0, NULL
, 'h'},
820 {"version", 0, NULL
, 'v'},
821 {"exec", 0, NULL
, 'e'},
825 while((opt
= getopt_long(argc
, argv
, "vhf:b:x:y:n:c:e:",
826 long_options
, NULL
)) != -1)
830 eprint_version(PROGNAME
);
833 exit_help(EXIT_SUCCESS
);
836 configfile
= a_strdup(optarg
);
839 globalconf
.exec
= a_strdup(optarg
);
843 if(argc
- optind
>= 1)
844 globalconf
.prompt
= a_strdup(argv
[optind
]);
846 globalconf
.connection
= xcb_connect(NULL
, &globalconf
.default_screen
);
847 if(xcb_connection_has_error(globalconf
.connection
))
848 eprint("unable to open display");
850 si
= screensinfo_new(globalconf
.connection
);
851 if(si
->xinerama_is_active
)
853 if((xqp
= xcb_query_pointer_reply(globalconf
.connection
,
854 xcb_query_pointer(globalconf
.connection
,
855 xutil_root_window(globalconf
.connection
,
856 globalconf
.default_screen
)),
859 screen
= screen_get_bycoord(si
, 0, xqp
->root_x
, xqp
->root_y
);
862 geometry
.x
= si
->geometry
[screen
].x
;
863 geometry
.y
= si
->geometry
[screen
].y
;
864 geometry
.width
= si
->geometry
[screen
].width
;
869 screen
= globalconf
.default_screen
;
871 geometry
.width
= xcb_aux_get_screen(globalconf
.connection
, globalconf
.default_screen
)->width_in_pixels
;
874 if((ret
= config_parse(screen
, configfile
, globalconf
.prompt
, &geometry
)))
877 /* Init the geometry */
879 geometry
.height
= 1.5 * MAX(globalconf
.styles
.normal
.font
->height
,
880 globalconf
.styles
.focus
.font
->height
);
882 screensinfo_delete(&si
);
884 /* Create the window */
885 globalconf
.sw
= simplewindow_new(globalconf
.connection
, globalconf
.default_screen
,
886 geometry
.x
, geometry
.y
, geometry
.width
, geometry
.height
, 0);
888 xcb_change_property(globalconf
.connection
, XCB_PROP_MODE_REPLACE
,
889 globalconf
.sw
->window
, WM_NAME
, STRING
, 8,
890 strlen(PROGNAME
), PROGNAME
);
892 /* Create the drawing context */
893 globalconf
.ctx
= draw_context_new(globalconf
.connection
, globalconf
.default_screen
,
894 geometry
.width
, geometry
.height
,
895 globalconf
.sw
->drawable
);
897 /* Allocate a default size for the text on the heap instead of
898 * using stack allocation with PATH_MAX (may not been defined
899 * according to POSIX). This string size may be increased if
901 globalconf
.text_complete
= globalconf
.text
= p_new(char, CHUNK_SIZE
);
902 globalconf
.text_size
= CHUNK_SIZE
;
904 if(isatty(STDIN_FILENO
))
906 if(!item_list_fill_stdin())
907 item_list_fill_file(NULL
);
913 if(!item_list_fill_stdin())
914 item_list_fill_file(NULL
);
921 globalconf
.keysyms
= xcb_key_symbols_alloc(globalconf
.connection
);
923 /* Get the numlock, capslock and shiftlock mask */
924 xutil_get_lock_mask(globalconf
.connection
, globalconf
.keysyms
, &globalconf
.numlockmask
,
925 &globalconf
.shiftlockmask
, &globalconf
.capslockmask
);
927 xutil_map_raised(globalconf
.connection
, globalconf
.sw
->window
);
931 while((ev
= xcb_poll_for_event(globalconf
.connection
)))
934 if(ev
->response_type
== 0)
937 switch(ev
->response_type
& 0x7f)
939 case XCB_BUTTON_PRESS
:
943 handle_kpress((xcb_key_press_event_t
*) ev
);
946 if(!((xcb_expose_event_t
*) ev
)->count
)
947 simplewindow_refresh_drawable(globalconf
.sw
, globalconf
.default_screen
);
960 printf("%s\n", globalconf
.text
);
964 shell
= a_strdup("/bin/sh");
965 len
= a_strlen(globalconf
.exec
) + a_strlen(globalconf
.text
) + 1;
966 cmd
= p_new(char, len
);
967 a_strcpy(cmd
, len
, globalconf
.exec
);
968 a_strcat(cmd
, len
, globalconf
.text
);
969 execl(shell
, shell
, "-c", cmd
, NULL
);
973 xcb_key_symbols_free(globalconf
.keysyms
);
974 p_delete(&globalconf
.text
);
975 draw_context_delete(&globalconf
.ctx
);
976 simplewindow_delete(&globalconf
.sw
);
977 xcb_disconnect(globalconf
.connection
);
982 // vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80