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.
22 /* getline(), asprintf() */
25 #define CHUNK_SIZE 4096
34 #include <sys/types.h>
39 #include <X11/Xutil.h>
41 #include "common/swindow.h"
42 #include "common/util.h"
43 #include "common/version.h"
44 #include "common/configopts.h"
45 #include "common/xutil.h"
46 #include "common/xscreen.c"
48 #define PROGNAME "awesome-menu"
50 #define CLEANMASK(mask) (mask & ~(globalconf.numlockmask | LockMask))
52 /** awesome-menu run status */
55 /** Stop awesome-menu */
57 /** Run awesome-menu */
59 /** Stop awesome-menu and cancel any operation */
63 /** Is awesome-menu running ? */
64 static status_t status
= RUN
;
66 /** Import awesome config file format */
67 extern cfg_opt_t awesome_opts
[];
70 typedef struct item_t item_t
;
71 /** Item_t structure */
76 /** Previous and next elems in item_t list */
78 /** True if the item currently matches */
82 /** Destructor for item structure
83 * \param item item pointer
86 item_delete(item_t
**item
)
88 p_delete(&(*item
)->data
);
92 DO_SLIST(item_t
, item
, item_delete
)
94 /** awesome-run global configuration structure */
101 /** The draw contet */
110 unsigned int numlockmask
;
113 /** The text when we asked to complete */
115 /** The text length */
120 item_t
*item_selected
;
121 /** What to do with the result text */
127 static AwesomeMenuConf globalconf
;
129 /** Exit with given exit code
130 * \param exit_code exit code
131 * \return never returns
133 static void __attribute__ ((noreturn
))
134 exit_help(int exit_code
)
136 FILE *outfile
= (exit_code
== EXIT_SUCCESS
) ? stdout
: stderr
;
137 fprintf(outfile
, "Usage: %s [-c config] [-e command] <message>\n",
142 /** Parse configuration file and fill up AwesomeMenuConf
143 * data structures with configuration directives.
144 * \param screen screen number
145 * \param confpatharg configuration file pathname, or NULL if auto
146 * \param menu_title menu title
147 * \param geometry geometry to fill up with supplied information from
149 * \return cfg_parse status
152 config_parse(int screen
, const char *confpatharg
,
153 const char *menu_title
, area_t
*geometry
)
157 cfg_t
*cfg
, *cfg_menu
= NULL
, *cfg_screen
= NULL
,
158 *cfg_styles
, *cfg_menu_styles
= NULL
;
161 confpath
= config_file();
163 confpath
= a_strdup(confpatharg
);
167 switch((ret
= cfg_parse(cfg
, confpath
)))
170 warn("parsing configuration file failed: %s\n", strerror(errno
));
172 case CFG_PARSE_ERROR
:
173 cfg_error(cfg
, "W: awesome: parsing configuration file %s failed.\n", confpath
);
180 if(menu_title
&& !(cfg_menu
= cfg_gettsec(cfg
, "menu", menu_title
)))
181 warn("no definition for menu %s in configuration file: using default\n",
184 /* get global screen section */
185 if(!(cfg_screen
= cfg_getnsec(cfg
, "screen", screen
)))
186 cfg_screen
= cfg_getsec(cfg
, "screen");
190 cfg_menu_styles
= cfg_getsec(cfg_menu
, "styles");
191 if((i
= cfg_getint(cfg_menu
, "x")) != (int) 0xffffffff)
193 if((i
= cfg_getint(cfg_menu
, "y")) != (int) 0xffffffff)
195 if((i
= cfg_getint(cfg_menu
, "width")) > 0)
197 if((i
= cfg_getint(cfg_menu
, "height")) > 0)
198 geometry
->height
= i
;
202 && (cfg_styles
= cfg_getsec(cfg_screen
, "styles")))
204 /* Grab default styles */
205 draw_style_init(globalconf
.display
, DefaultScreen(globalconf
.display
),
206 cfg_getsec(cfg_styles
, "normal"),
207 &globalconf
.styles
.normal
, NULL
);
209 draw_style_init(globalconf
.display
, DefaultScreen(globalconf
.display
),
210 cfg_getsec(cfg_styles
, "focus"),
211 &globalconf
.styles
.focus
, &globalconf
.styles
.normal
);
214 /* Now grab menu styles if any */
217 draw_style_init(globalconf
.display
, DefaultScreen(globalconf
.display
),
218 cfg_getsec(cfg_menu_styles
, "normal"),
219 &globalconf
.styles
.normal
, NULL
);
221 draw_style_init(globalconf
.display
, DefaultScreen(globalconf
.display
),
222 cfg_getsec(cfg_menu_styles
, "focus"),
223 &globalconf
.styles
.focus
, &globalconf
.styles
.normal
);
226 if(!globalconf
.styles
.normal
.font
)
227 eprint("no default font available\n");
234 /** Return the last word for a text.
235 * \param text the text to look into
236 * \return a pointer to the last word position in text
239 get_last_word(char *text
)
243 if((last_word
= strrchr(text
, ' ')))
251 /** Fill the completion list for awesome-menu with file list.
252 * \param directory directory to look into
253 * \return always true
256 item_list_fill_file(const char *directory
)
258 char *cwd
, *home
, *user
, *filename
;
261 struct dirent
*dirinfo
;
263 ssize_t len
, lenfile
;
264 struct passwd
*passwd
= NULL
;
267 item_list_wipe(&globalconf
.items
);
270 cwd
= a_strdup("./");
271 else if(a_strlen(directory
) > 1 && directory
[0] == '~')
273 if(directory
[1] == '/')
275 home
= getenv("HOME");
276 asprintf(&cwd
, "%s%s", (home
? home
: ""), directory
+ 1);
280 if(!(file
= strchr(directory
, '/')))
281 file
= directory
+ a_strlen(directory
);
282 len
= (file
- directory
) + 1;
283 user
= p_new(char, len
);
284 a_strncpy(user
, len
, directory
+ 1, (file
- directory
) - 1);
285 if((passwd
= getpwnam(user
)))
287 asprintf(&cwd
, "%s%s", passwd
->pw_dir
, file
);
298 cwd
= a_strdup(directory
);
300 if(!(dir
= opendir(cwd
)))
306 while((dirinfo
= readdir(dir
)))
308 item
= p_new(item_t
, 1);
310 /* + 1 for \0 + 1 for / if directory */
311 len
= a_strlen(directory
) + a_strlen(dirinfo
->d_name
) + 2;
313 item
->data
= p_new(char, len
);
314 if(a_strlen(directory
))
315 a_strcpy(item
->data
, len
, directory
);
316 a_strcat(item
->data
, len
, dirinfo
->d_name
);
318 lenfile
= a_strlen(cwd
) + a_strlen(dirinfo
->d_name
) + 2;
320 filename
= p_new(char, lenfile
);
321 a_strcpy(filename
, lenfile
, cwd
);
322 a_strcat(filename
, lenfile
, dirinfo
->d_name
);
324 if(!stat(filename
, &st
) && S_ISDIR(st
.st_mode
))
325 a_strcat(item
->data
, len
, "/");
329 item_list_push(&globalconf
.items
, item
);
339 complete(Bool reverse
)
343 item_t
*(*item_iter
)(item_t
**, item_t
*) = item_list_next_cycle
;
346 item_iter
= item_list_prev_cycle
;
348 if(globalconf
.item_selected
)
349 item
= item_iter(&globalconf
.items
, globalconf
.item_selected
);
351 item
= globalconf
.items
;
353 for(; item
&& loop
; item
= item_iter(&globalconf
.items
, item
))
357 a_strcpy(globalconf
.text_complete
,
358 globalconf
.text_size
- (globalconf
.text_complete
- globalconf
.text
),
360 globalconf
.item_selected
= item
;
363 /* Since loop is 2, it will be 1 at first iter, and then 0 if we
364 * get back before matching an item (i.e. no items match) to the
365 * first elem: so it will break the loop, otherwise it loops for
368 if(item
== globalconf
.items
)
373 /** Compute a match from completion list for word.
374 * \param word the word to match
377 compute_match(const char *word
)
379 ssize_t len
= a_strlen(word
);
382 /* reset the selected item to NULL */
383 globalconf
.item_selected
= NULL
;
387 if(word
[len
- 1] == '/'
388 || word
[len
- 1] == ' ')
389 item_list_fill_file(word
);
391 for(item
= globalconf
.items
; item
; item
= item
->next
)
392 if(!a_strncmp(word
, item
->data
, a_strlen(word
)))
399 if(a_strlen(globalconf
.text
))
400 item_list_fill_file(NULL
);
401 for(item
= globalconf
.items
; item
; item
= item
->next
)
409 /** Redraw the menu. */
414 area_t geometry
= { 0, 0, 0, 0, NULL
, NULL
};
415 Bool selected_item_is_drawn
= False
;
416 int len
, prompt_len
, x_of_previous_item
;
419 geometry
.width
= globalconf
.sw
->geometry
.width
;
420 geometry
.height
= globalconf
.sw
->geometry
.height
;
422 if(a_strlen(globalconf
.prompt
))
424 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
425 MARGIN
, globalconf
.prompt
, globalconf
.styles
.focus
);
427 len
= MARGIN
* 2 + draw_textwidth(globalconf
.display
, globalconf
.styles
.focus
.font
, globalconf
.prompt
);
429 geometry
.width
-= len
;
432 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
433 MARGIN
, globalconf
.text
, globalconf
.styles
.normal
);
435 len
= MARGIN
* 2 + MAX(draw_textwidth(globalconf
.display
, globalconf
.styles
.normal
.font
, globalconf
.text
),
436 geometry
.width
/ 20);
438 geometry
.width
-= len
;
439 prompt_len
= geometry
.x
;
441 for(item
= globalconf
.items
; item
&& geometry
.width
> 0; item
= item
->next
)
444 style
= item
== globalconf
.item_selected
? globalconf
.styles
.focus
: globalconf
.styles
.normal
;
445 len
= MARGIN
+ draw_textwidth(globalconf
.display
, style
.font
, item
->data
);
446 if(item
== globalconf
.item_selected
)
448 if(len
> geometry
.width
)
451 selected_item_is_drawn
= True
;
453 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
454 MARGIN
/ 2, item
->data
, style
);
456 geometry
.width
-= len
;
459 /* we have an item selected but not drawn, so redraw in the other side */
460 if(globalconf
.item_selected
&& !selected_item_is_drawn
)
462 geometry
.x
= globalconf
.sw
->geometry
.width
;
464 for(item
= globalconf
.item_selected
; item
; item
= item_list_prev(&globalconf
.items
, item
))
467 style
= item
== globalconf
.item_selected
? globalconf
.styles
.focus
: globalconf
.styles
.normal
;
468 x_of_previous_item
= geometry
.x
;
469 geometry
.width
= MARGIN
+ draw_textwidth(globalconf
.display
, style
.font
, item
->data
);
470 geometry
.x
-= geometry
.width
;
472 if(geometry
.x
< prompt_len
)
475 draw_text(globalconf
.ctx
, geometry
, AlignLeft
,
476 MARGIN
/ 2, item
->data
, style
);
481 geometry
.x
= prompt_len
;
482 geometry
.width
= x_of_previous_item
- prompt_len
;
483 draw_rectangle(globalconf
.ctx
, geometry
, 1.0, True
, globalconf
.styles
.normal
.bg
);
486 else if(geometry
.width
)
487 draw_rectangle(globalconf
.ctx
, geometry
, 1.0, True
, globalconf
.styles
.normal
.bg
);
489 simplewindow_refresh_drawable(globalconf
.sw
, DefaultScreen(globalconf
.display
));
490 XSync(globalconf
.display
, False
);
493 /** Handle keypress event in awesome-menu.
494 * \param e received XKeyEvent
497 handle_kpress(XKeyEvent
*e
)
505 len
= a_strlen(globalconf
.text
);
506 num
= XLookupString(e
, buf
, sizeof(buf
), &ksym
, 0);
507 if(e
->state
& ControlMask
)
537 while(i
>= 0 && globalconf
.text
[i
] == ' ')
538 globalconf
.text
[i
--] = '\0';
539 while(i
>= 0 && globalconf
.text
[i
] != ' ')
540 globalconf
.text
[i
--] = '\0';
541 compute_match(get_last_word(globalconf
.text
));
546 else if(CLEANMASK(e
->state
) & Mod1Mask
)
562 globalconf
.text_complete
= globalconf
.text
+ a_strlen(globalconf
.text
) + 1;
564 if(num
&& !iscntrl((int) buf
[0]))
566 if(buf
[0] != '/' || globalconf
.text
[len
- 1] != '/')
570 /* Reallocate text string if needed to hold
571 * concatenation of text and buf */
572 if((text_dst_len
= (a_strlen(globalconf
.text
) + num
- 1)) > globalconf
.text_size
)
574 globalconf
.text_size
+= ((int) (text_dst_len
/ globalconf
.text_size
)) * CHUNK_SIZE
;
575 p_realloc(&globalconf
.text
, globalconf
.text_size
);
577 a_strncat(globalconf
.text
, globalconf
.text_size
, buf
, num
);
579 compute_match(get_last_word(globalconf
.text
));
586 globalconf
.text
[--len
] = '\0';
587 compute_match(get_last_word(globalconf
.text
));
591 case XK_ISO_Left_Tab
:
610 /** Fill the completion by reading on stdin.
611 * \return true if something has been filled up, false otherwise.
614 item_list_fill_stdin(void)
621 Bool has_entry
= False
;
623 item_list_init(&globalconf
.items
);
625 if((line_len
= getline(&buf
, &len
, stdin
)) != -1)
631 buf
[line_len
- 1] = '\0';
632 newitem
= p_new(item_t
, 1);
633 newitem
->data
= a_strdup(buf
);
634 newitem
->match
= True
;
635 item_list_append(&globalconf
.items
, newitem
);
637 while((line_len
= getline(&buf
, &len
, stdin
)) != -1);
645 /** Grab the keyboard.
651 for(i
= 1000; i
; i
--)
653 if(XGrabKeyboard(globalconf
.display
, DefaultRootWindow(globalconf
.display
), True
,
654 GrabModeAsync
, GrabModeAsync
, CurrentTime
) == GrabSuccess
)
659 eprint("cannot grab keyboard");
662 /** Main function of awesome-menu.
663 * \param argc number of elements in argv
664 * \param argv arguments array
665 * \return EXIT_SUCCESS
668 main(int argc
, char **argv
)
671 int opt
, ret
, x
, y
, i
, screen
= 0;
672 char *configfile
= NULL
, *cmd
;
674 const char *shell
= getenv("SHELL");
675 area_t geometry
= { 0, 0, 0, 0, NULL
, NULL
};
679 static struct option long_options
[] =
681 {"help", 0, NULL
, 'h'},
682 {"version", 0, NULL
, 'v'},
683 {"exec", 0, NULL
, 'e'},
687 while((opt
= getopt_long(argc
, argv
, "vhf:b:x:y:n:c:e:",
688 long_options
, NULL
)) != -1)
692 eprint_version(PROGNAME
);
695 exit_help(EXIT_SUCCESS
);
698 configfile
= a_strdup(optarg
);
701 globalconf
.exec
= a_strdup(optarg
);
705 if(argc
- optind
>= 1)
706 globalconf
.prompt
= a_strdup(argv
[optind
]);
708 if(!(globalconf
.display
= XOpenDisplay(NULL
)))
709 eprint("unable to open display");
711 /* Get the numlock mask */
712 globalconf
.numlockmask
= xgetnumlockmask(globalconf
.display
);
714 si
= screensinfo_new(globalconf
.display
);
715 if(si
->xinerama_is_active
)
717 if(XQueryPointer(globalconf
.display
, RootWindow(globalconf
.display
, DefaultScreen(globalconf
.display
)),
718 &dummy
, &dummy
, &x
, &y
, &i
, &i
, &ui
))
720 screen
= screen_get_bycoord(si
, 0, x
, y
);
722 geometry
.x
= si
->geometry
[screen
].x
;
723 geometry
.y
= si
->geometry
[screen
].y
;
724 geometry
.width
= si
->geometry
[screen
].width
;
729 screen
= DefaultScreen(globalconf
.display
);
731 geometry
.width
= DisplayWidth(globalconf
.display
, DefaultScreen(globalconf
.display
));
734 if((ret
= config_parse(screen
, configfile
, globalconf
.prompt
, &geometry
)))
737 /* Init the geometry */
739 geometry
.height
= 1.5 * MAX(globalconf
.styles
.normal
.font
->height
,
740 globalconf
.styles
.focus
.font
->height
);
742 screensinfo_delete(&si
);
744 /* Create the window */
745 globalconf
.sw
= simplewindow_new(globalconf
.display
, DefaultScreen(globalconf
.display
),
746 geometry
.x
, geometry
.y
, geometry
.width
, geometry
.height
, 0);
748 XStoreName(globalconf
.display
, globalconf
.sw
->window
, PROGNAME
);
750 /* Create the drawing context */
751 globalconf
.ctx
= draw_context_new(globalconf
.display
, DefaultScreen(globalconf
.display
),
752 geometry
.width
, geometry
.height
,
753 globalconf
.sw
->drawable
);
755 /* Allocate a default size for the text on the heap instead of
756 * using stack allocation with PATH_MAX (may not been defined
757 * according to POSIX). This string size may be increased if
759 globalconf
.text_complete
= globalconf
.text
= p_new(char, CHUNK_SIZE
);
760 globalconf
.text_size
= CHUNK_SIZE
;
762 if(isatty(STDIN_FILENO
))
764 if(!item_list_fill_stdin())
765 item_list_fill_file(NULL
);
771 if(!item_list_fill_stdin())
772 item_list_fill_file(NULL
);
779 XMapRaised(globalconf
.display
, globalconf
.sw
->window
);
783 XNextEvent(globalconf
.display
, &ev
);
790 handle_kpress(&ev
.xkey
);
793 if(!ev
.xexpose
.count
)
794 simplewindow_refresh_drawable(globalconf
.sw
, DefaultScreen(globalconf
.display
));
804 printf("%s\n", globalconf
.text
);
808 shell
= a_strdup("/bin/sh");
809 len
= a_strlen(globalconf
.exec
) + a_strlen(globalconf
.text
) + 1;
810 cmd
= p_new(char, len
);
811 a_strcpy(cmd
, len
, globalconf
.exec
);
812 a_strcat(cmd
, len
, globalconf
.text
);
813 execl(shell
, shell
, "-c", cmd
, NULL
);
817 p_delete(&globalconf
.text
);
818 draw_context_delete(&globalconf
.ctx
);
819 simplewindow_delete(&globalconf
.sw
);
820 XCloseDisplay(globalconf
.display
);
825 // vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80