#include <X11/Xresource.h>
#include <X11/extensions/Xinerama.h>
#define MAXPATHS 128 /* maximal number of paths to look for icons */
#define ICONPATH "ICONPATH" /* environment variable name */
#define LEN(x) (sizeof (x) / sizeof (x[0]))
#define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b))
if ((__TMP__ = strtoul((s), NULL, 10)) < INT_MAX) \
/* Actions for the main loop */
ACTION_CLEAR
= 1<<0, /* clear text */
ACTION_SELECT
= 1<<1, /* select item */
ACTION_MAP
= 1<<2, /* remap menu windows */
ACTION_DRAW
= 1<<3, /* redraw menu windows */
ACTION_WARP
= 1<<4, /* warp the pointer */
/* enum for keyboard menu navigation */
enum { ITEMPREV
, ITEMNEXT
, ITEMFIRST
, ITEMLAST
};
/* enum for text alignment */
enum {LeftAlignment
, CenterAlignment
, RightAlignment
};
enum {ColorFG
, ColorBG
, ColorLast
};
enum {NetWMName
, NetWMWindowType
, NetWMWindowTypePopupMenu
, NetLast
};
/* configuration structure */
/* the values below are set by config.h */
const char *background_color
;
const char *foreground_color
;
const char *selbackground_color
;
const char *selforeground_color
;
const char *separator_color
;
const char *border_color
;
/* the values below are set by options */
int posx
, posy
; /* rootmenu position */
/* the value below is computed by xmenu */
/* draw context structure */
XftColor normal
[ColorLast
];
XftColor selected
[ColorLast
];
/* menu item structure */
char *label
; /* string to be drawed on menu */
char *output
; /* string to be outputed when item is clicked */
char *file
; /* filename of the icon */
int y
; /* item y position relative to menu */
int textw
; /* text width */
struct Item
*prev
; /* previous item */
struct Item
*next
; /* next item */
struct Menu
*submenu
; /* submenu spawned by clicking on item */
Drawable sel
, unsel
; /* pixmap for selected and unselected item */
/* monitor geometry structure */
int x
, y
, w
, h
; /* monitor geometry */
struct Menu
*parent
; /* parent menu */
struct Item
*caller
; /* item that spawned the menu */
struct Item
*list
; /* list of items contained by the menu */
struct Item
*first
; /* first item displayed on the menu */
struct Item
*selected
; /* item currently selected in the menu */
int x
, y
, w
, h
; /* menu geometry */
int hasicon
; /* whether the menu has item with icons */
int drawn
; /* whether the menu was already drawn */
int maxtextw
; /* maximum text width */
int level
; /* menu level relative to root */
int overflow
; /* whether the menu is higher than the monitor */
Window win
; /* menu window to map on the screen */
XIC xic
; /* input context */
static Colormap colormap
;
static XClassHint classh
;
static Atom netatom
[NetLast
];
static int iflag
= 0; /* whether to disable icons */
static int rflag
= 0; /* whether to disable right-click */
static int mflag
= 0; /* whether the user specified a monitor with -p */
static int pflag
= 0; /* whether the user specified a position with -p */
static int wflag
= 0; /* whether to let the window manager control XMenu */
static int rootmodeflag
= 0; /* wheter to run in root mode */
static int passclickflag
= 0; /* whether to pass click to root window */
static int firsttime
= 1; /* set to 0 after first run */
static unsigned int button
= 0; /* button to trigger pmenu in root mode */
static unsigned int modifier
= 0; /* modifier to trigger pmenu */
static char *iconstring
= NULL
; /* string read from getenv */
static char *iconpaths
[MAXPATHS
]; /* paths to icon directories */
static int niconpaths
= 0; /* number of paths to icon directories */
/* include config variable */
(void)fprintf(stderr
, "usage: xmenu [-irw] [-p position] [(-x|-X) [modifier-]button] [title]\n");
/* call malloc checking for error */
if ((p
= malloc(size
)) == NULL
)
/* call strdup checking for error */
if ((t
= strdup(s
)) == NULL
)
/* parse position string from -p, put results on config.posx, config.posy, and config.monitor */
parseposition(char *optarg
)
n
= strtol(s
, &endp
, 10);
if (errno
== ERANGE
|| n
> INT_MAX
|| n
< 0 || endp
== s
|| *endp
!= 'x')
n
= strtol(s
, &endp
, 10);
if (errno
== ERANGE
|| n
> INT_MAX
|| n
< 0 || endp
== s
)
if (strncasecmp(s
, "CUR", 3) == 0) {
n
= strtol(s
, &endp
, 10);
if (errno
== ERANGE
|| n
> INT_MAX
|| n
< 0 || endp
== s
|| *endp
!= '\0')
} else if (*endp
!= '\0') {
errx(1, "improper position: %s", optarg
);
/* get configuration from X resources */
if (xrm
== NULL
|| xdb
== NULL
)
if (XrmGetResource(xdb
, "xmenu.borderWidth", "*", &type
, &xval
) == True
)
GETNUM(config
.border_pixels
, xval
.addr
)
if (XrmGetResource(xdb
, "xmenu.separatorWidth", "*", &type
, &xval
) == True
)
GETNUM(config
.separator_pixels
, xval
.addr
)
if (XrmGetResource(xdb
, "xmenu.height", "*", &type
, &xval
) == True
)
GETNUM(config
.height_pixels
, xval
.addr
)
if (XrmGetResource(xdb
, "xmenu.width", "*", &type
, &xval
) == True
)
GETNUM(config
.width_pixels
, xval
.addr
)
if (XrmGetResource(xdb
, "xmenu.gap", "*", &type
, &xval
) == True
)
GETNUM(config
.gap_pixels
, xval
.addr
)
if (XrmGetResource(xdb
, "xmenu.maxItems", "*", &type
, &xval
) == True
)
GETNUM(config
.max_items
, xval
.addr
)
if (XrmGetResource(xdb
, "xmenu.background", "*", &type
, &xval
) == True
)
config
.background_color
= xval
.addr
;
if (XrmGetResource(xdb
, "xmenu.foreground", "*", &type
, &xval
) == True
)
config
.foreground_color
= xval
.addr
;
if (XrmGetResource(xdb
, "xmenu.selbackground", "*", &type
, &xval
) == True
)
config
.selbackground_color
= xval
.addr
;
if (XrmGetResource(xdb
, "xmenu.selforeground", "*", &type
, &xval
) == True
)
config
.selforeground_color
= xval
.addr
;
if (XrmGetResource(xdb
, "xmenu.separator", "*", &type
, &xval
) == True
)
config
.separator_color
= xval
.addr
;
if (XrmGetResource(xdb
, "xmenu.border", "*", &type
, &xval
) == True
)
config
.border_color
= xval
.addr
;
if (XrmGetResource(xdb
, "xmenu.font", "*", &type
, &xval
) == True
)
if (XrmGetResource(xdb
, "xmenu.alignment", "*", &type
, &xval
) == True
) {
if (strcasecmp(xval
.addr
, "center") == 0)
config
.alignment
= CenterAlignment
;
else if (strcasecmp(xval
.addr
, "left") == 0)
config
.alignment
= LeftAlignment
;
else if (strcasecmp(xval
.addr
, "right") == 0)
config
.alignment
= RightAlignment
;
/* set button global variable */
if ((len
= strlen(s
)) < 1)
case '1': button
= Button1
; break;
case '2': button
= Button2
; break;
case '3': button
= Button3
; break;
default: button
= atoi(&s
[len
-1]); break;
/* set modifier global variable */
if ((len
= strlen(s
)) < 1)
case '1': modifier
= Mod1Mask
; break;
case '2': modifier
= Mod2Mask
; break;
case '3': modifier
= Mod3Mask
; break;
case '4': modifier
= Mod4Mask
; break;
case '5': modifier
= Mod5Mask
; break;
if (strcasecmp(s
, "Alt") == 0) {
} else if (strcasecmp(s
, "Super") == 0) {
/* parse icon path string */
for (s
= strtok(iconstring
, ":"); s
!= NULL
; s
= strtok(NULL
, ":")) {
if (niconpaths
< MAXPATHS
) {
iconpaths
[niconpaths
++] = s
;
/* get configuration from command-line options */
getoptions(int argc
, char *argv
[])
classh
.res_class
= CLASS
;
classh
.res_name
= argv
[0];
if ((s
= strrchr(argv
[0], '/')) != NULL
)
parseiconpaths(getenv(ICONPATH
));
while ((ch
= getopt(argc
, argv
, "ip:rwx:X:")) != -1) {
if ((t
= strchr(s
, '-')) == NULL
)
parsefonts(const char *s
)
if ((dc
.fonts
= calloc(dc
.nfonts
, sizeof *dc
.fonts
)) == NULL
)
while (i
< sizeof buf
&& *p
!= '\0' && *p
!= ',')
errx(1, "font name too long");
if ((dc
.pattern
= FcNameParse((FcChar8
*)buf
)) == NULL
)
errx(1, "the first font in the cache must be loaded from a font string");
if ((dc
.fonts
[nfont
++] = XftFontOpenName(dpy
, screen
, buf
)) == NULL
)
errx(1, "could not load font");
/* get color from color string */
ealloccolor(const char *s
, XftColor
*color
)
if(!XftColorAllocName(dpy
, visual
, colormap
, s
, color
))
errx(1, "could not allocate color: %s", s
);
/* query monitor information and cursor position */
getmonitor(struct Monitor
*mon
)
XineramaScreenInfo
*info
= NULL
;
Window dw
; /* dummy variable */
int di
; /* dummy variable */
unsigned du
; /* dummy variable */
int cursx
, cursy
; /* cursor position */
XQueryPointer(dpy
, rootwin
, &dw
, &dw
, &cursx
, &cursy
, &di
, &di
, &du
);
mon
->w
= DisplayWidth(dpy
, screen
);
mon
->h
= DisplayHeight(dpy
, screen
);
if ((info
= XineramaQueryScreens(dpy
, &nmons
)) != NULL
) {
if (!mflag
|| config
.monitor
< 0 || config
.monitor
>= nmons
) {
for (i
= 0; i
< nmons
; i
++) {
if (BETWEEN(cursx
, info
[i
].x_org
, info
[i
].x_org
+ info
[i
].width
) &&
BETWEEN(cursy
, info
[i
].y_org
, info
[i
].y_org
+ info
[i
].height
)) {
mon
->x
= info
[selmon
].x_org
;
mon
->y
= info
[selmon
].y_org
;
mon
->w
= info
[selmon
].width
;
mon
->h
= info
[selmon
].height
;
ealloccolor(config
.background_color
, &dc
.normal
[ColorBG
]);
ealloccolor(config
.foreground_color
, &dc
.normal
[ColorFG
]);
ealloccolor(config
.selbackground_color
, &dc
.selected
[ColorBG
]);
ealloccolor(config
.selforeground_color
, &dc
.selected
[ColorFG
]);
ealloccolor(config
.separator_color
, &dc
.separator
);
ealloccolor(config
.border_color
, &dc
.border
);
dc
.gc
= XCreateGC(dpy
, rootwin
, 0, NULL
);
/* calculate icon size */
config
.iconsize
= config
.height_pixels
- config
.iconpadding
* 2;
utf8string
= XInternAtom(dpy
, "UTF8_STRING", False
);
wmdelete
= XInternAtom(dpy
, "WM_DELETE_WINDOW", False
);
netatom
[NetWMName
] = XInternAtom(dpy
, "_NET_WM_NAME", False
);
netatom
[NetWMWindowType
] = XInternAtom(dpy
, "_NET_WM_WINDOW_TYPE", False
);
netatom
[NetWMWindowTypePopupMenu
] = XInternAtom(dpy
, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False
);
allocitem(const char *label
, const char *output
, char *file
)
item
= emalloc(sizeof *item
);
item
->label
= estrdup(label
);
item
->output
= item
->label
;
item
->output
= estrdup(output
);
item
->file
= estrdup(file
);
/* allocate a menu and create its window */
allocmenu(struct Menu
*parent
, struct Item
*list
, int level
)
XSetWindowAttributes swa
;
menu
= emalloc(sizeof *menu
);
swa
.override_redirect
= (wflag
) ? False
: True
;
swa
.background_pixel
= dc
.normal
[ColorBG
].pixel
;
swa
.border_pixel
= dc
.border
.pixel
;
swa
.save_under
= True
; /* pop-up windows should save_under*/
swa
.event_mask
= ExposureMask
| KeyPressMask
| ButtonPressMask
| ButtonReleaseMask
| PointerMotionMask
| LeaveWindowMask
;
swa
.event_mask
|= StructureNotifyMask
;
menu
->win
= XCreateWindow(dpy
, rootwin
, 0, 0, 1, 1, config
.border_pixels
,
CopyFromParent
, CopyFromParent
, CopyFromParent
,
CWOverrideRedirect
| CWBackPixel
|
CWBorderPixel
| CWEventMask
| CWSaveUnder
,
menu
->xic
= XCreateIC(xim
, XNInputStyle
, XIMPreeditNothing
| XIMStatusNothing
,
XNClientWindow
, menu
->win
, XNFocusWindow
, menu
->win
, NULL
);
errx(1, "XCreateIC: could not obtain input method");
/* build the menu tree */
buildmenutree(int level
, const char *label
, const char *output
, char *file
)
static struct Menu
*prevmenu
= NULL
; /* menu the previous item was added to */
static struct Menu
*rootmenu
= NULL
; /* menu to be returned */
struct Item
*curritem
= NULL
; /* item currently being read */
struct Item
*item
; /* dummy item for loops */
struct Menu
*menu
; /* dummy menu for loops */
curritem
= allocitem(label
, output
, file
);
/* put the item in the menu tree */
if (prevmenu
== NULL
) { /* there is no menu yet */
menu
= allocmenu(NULL
, curritem
, level
);
} else if (level
< prevmenu
->level
) { /* item is continuation of a parent menu */
/* go up the menu tree until find the menu this item continues */
for (menu
= prevmenu
, i
= level
;
menu
!= NULL
&& i
!= prevmenu
->level
;
menu
= menu
->parent
, i
++)
errx(1, "improper indentation detected");
/* find last item in the new menu */
for (item
= menu
->list
; item
->next
!= NULL
; item
= item
->next
)
} else if (level
== prevmenu
->level
) { /* item is a continuation of current menu */
/* find last item in the previous menu */
for (item
= prevmenu
->list
; item
->next
!= NULL
; item
= item
->next
)
} else if (level
> prevmenu
->level
) { /* item begins a new menu */
menu
= allocmenu(prevmenu
, curritem
, level
);
/* find last item in the previous menu */
for (item
= prevmenu
->list
; item
->next
!= NULL
; item
= item
->next
)
/* a separator is no valid root for a submenu */
errx(1, "a separator is no valid root for a submenu");
/* create menus and items from the stdin */
char *file
, *label
, *output
;
while (fgets(buf
, BUFSIZ
, stdin
) != NULL
) {
/* get the indentation level */
level
= strspn(buf
, "\t");
label
= strtok(s
, "\t\n");
if (label
!= NULL
&& strncmp(label
, "IMG:", 4) == 0) {
label
= strtok(NULL
, "\t\n");
output
= strtok(NULL
, "\n");
rootmenu
= buildmenutree(level
, label
, output
, file
);
/* get next utf8 char from s return its codepoint and set next_ret to pointer to end of character */
getnextutf8char(const char *s
, const char **next_ret
)
static const unsigned char utfbyte
[] = {0x80, 0x00, 0xC0, 0xE0, 0xF0};
static const unsigned char utfmask
[] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
static const FcChar32 utfmin
[] = {0, 0x00, 0x80, 0x800, 0x10000};
static const FcChar32 utfmax
[] = {0, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
/* 0xFFFD is the replacement character, used to represent unknown characters */
static const FcChar32 unknown
= 0xFFFD;
FcChar32 ucode
; /* FcChar32 type holds 32 bits */
size_t usize
= 0; /* n' of bytes of the utf8 character */
/* get code of first byte of utf8 character */
for (i
= 0; i
< sizeof utfmask
; i
++) {
if (((unsigned char)*s
& utfmask
[i
]) == utfbyte
[i
]) {
ucode
= (unsigned char)*s
& ~utfmask
[i
];
/* if first byte is a continuation byte or is not allowed, return unknown */
if (i
== sizeof utfmask
|| usize
== 0)
/* check the other usize-1 bytes */
for (i
= 1; i
< usize
; i
++) {
/* if byte is nul or is not a continuation byte, return unknown */
if (*s
== '\0' || ((unsigned char)*s
& utfmask
[0]) != utfbyte
[0])
/* 6 is the number of relevant bits in the continuation byte */
ucode
= (ucode
<< 6) | ((unsigned char)*s
& ~utfmask
[0]);
/* check if ucode is invalid or in utf-16 surrogate halves */
if (!BETWEEN(ucode
, utfmin
[usize
], utfmax
[usize
])
|| BETWEEN (ucode
, 0xD800, 0xDFFF))
/* get which font contains a given code point */
getfontucode(FcChar32 ucode
)
FcCharSet
*fccharset
= NULL
;
FcPattern
*fcpattern
= NULL
;
for (i
= 0; i
< dc
.nfonts
; i
++)
if (XftCharExists(dpy
, dc
.fonts
[i
], ucode
) == FcTrue
)
/* create a charset containing our code point */
fccharset
= FcCharSetCreate();
FcCharSetAddChar(fccharset
, ucode
);
/* create a pattern akin to the dc.pattern but containing our charset */
fcpattern
= FcPatternDuplicate(dc
.pattern
);
FcPatternAddCharSet(fcpattern
, FC_CHARSET
, fccharset
);
/* find pattern matching fcpattern */
FcConfigSubstitute(NULL
, fcpattern
, FcMatchPattern
);
FcDefaultSubstitute(fcpattern
);
match
= XftFontMatch(dpy
, screen
, fcpattern
, &result
);
/* if found a pattern, open its font */
retfont
= XftFontOpenPattern(dpy
, match
);
if (retfont
&& XftCharExists(dpy
, retfont
, ucode
) == FcTrue
) {
if ((dc
.fonts
= realloc(dc
.fonts
, dc
.nfonts
+1)) == NULL
)
dc
.fonts
[dc
.nfonts
] = retfont
;
return dc
.fonts
[dc
.nfonts
++];
XftFontClose(dpy
, retfont
);
/* in case no fount was found, return the first one */
/* draw text into XftDraw, return width of text glyphs */
drawtext(XftDraw
*draw
, XftColor
*color
, int x
, int y
, unsigned h
, const char *text
)
ucode
= getnextutf8char(text
, &next
);
currfont
= getfontucode(ucode
);
XftTextExtentsUtf8(dpy
, currfont
, (XftChar8
*)text
, len
, &ext
);
texty
= y
+ (h
- (currfont
->ascent
+ currfont
->descent
))/2 + currfont
->ascent
;
XftDrawStringUtf8(draw
, color
, currfont
, x
, texty
, (XftChar8
*)text
, len
);
/* setup the height, width and icon of the items of a menu */
setupitems(struct Menu
*menu
, struct Monitor
*mon
)
menu
->first
= menu
->list
;
menu
->w
= config
.width_pixels
;
maxh
= config
.max_items
> 3 ? (2 + config
.max_items
) * config
.height_pixels
: mon
->h
;
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
) {
if (item
->label
== NULL
) /* height for separator item */
item
->h
= config
.separator_pixels
;
item
->h
= config
.height_pixels
;
if (menu
->h
+ config
.height_pixels
* 2 < maxh
- config
.border_pixels
* 2) {
menu
->h
+= config
.height_pixels
* 2;
item
->textw
= drawtext(NULL
, NULL
, 0, 0, 0, item
->label
);
* the item width depends on the size of its label (item->textw),
* and it is only used to calculate the width of the menu (which
* is equal to the width of the largest item).
* the horizontal padding appears 4 times through the width of a
* item: before and after its icon, and before and after its triangle.
* if the iflag is set (icons are disabled) then the horizontal
* padding appears 3 times: before the label and around the triangle.
itemwidth
= item
->textw
+ config
.triangle_width
+ config
.horzpadding
* 3;
itemwidth
+= (iflag
|| !menu
->hasicon
) ? 0 : config
.iconsize
+ config
.horzpadding
;
menu
->w
= max(menu
->w
, itemwidth
);
menu
->maxtextw
= max(menu
->maxtextw
, item
->textw
);
XSetWindowBackground(dpy
, menu
->win
, dc
.normal
[ColorBG
].pixel
);
pix
= XCreatePixmap(dpy
, menu
->win
, menu
->w
, menu
->h
, depth
);
XSetForeground(dpy
, dc
.gc
, dc
.normal
[ColorBG
].pixel
);
XFillRectangle(dpy
, pix
, dc
.gc
, 0, 0, menu
->w
, menu
->h
);
XSetForeground(dpy
, dc
.gc
, dc
.normal
[ColorFG
].pixel
);
{menu
->w
/ 2 - 3, config
.height_pixels
/ 2 + 2},
4, Convex
, CoordModePrevious
{menu
->w
/ 2 - 3, menu
->h
- config
.height_pixels
/ 2 - 2},
4, Convex
, CoordModePrevious
XSetWindowBackgroundPixmap(dpy
, menu
->win
, pix
);
/* setup the position of a menu */
setupmenupos(struct Menu
*menu
, struct Monitor
*mon
)
width
= menu
->w
+ config
.border_pixels
* 2;
height
= menu
->h
+ config
.border_pixels
* 2;
if (menu
->parent
== NULL
) { /* if root menu, calculate in respect to cursor */
if (pflag
|| (config
.posx
>= mon
->x
&& mon
->x
+ mon
->w
- config
.posx
>= width
))
else if (config
.posx
> width
)
menu
->x
= config
.posx
- width
;
if (pflag
|| (config
.posy
>= mon
->y
&& mon
->y
+ mon
->h
- config
.posy
>= height
))
else if (mon
->y
+ mon
->h
> height
)
menu
->y
= mon
->y
+ mon
->h
- height
;
} else { /* else, calculate in respect to parent menu */
parentwidth
= menu
->parent
->x
+ menu
->parent
->w
+ config
.border_pixels
+ config
.gap_pixels
;
if (mon
->x
+ mon
->w
- parentwidth
>= width
)
else if (menu
->parent
->x
> menu
->w
+ config
.border_pixels
+ config
.gap_pixels
)
menu
->x
= menu
->parent
->x
- menu
->w
- config
.border_pixels
- config
.gap_pixels
;
if (mon
->y
+ mon
->h
- (menu
->caller
->y
+ menu
->parent
->y
) >= height
)
menu
->y
= menu
->caller
->y
+ menu
->parent
->y
;
else if (mon
->y
+ mon
->h
> height
)
menu
->y
= mon
->y
+ mon
->h
- height
;
/* recursivelly setup menu configuration and its pixmap */
setupmenu(struct Menu
*menu
, struct Monitor
*mon
)
/* setup size and position of menus */
/* update menu geometry */
changes
.height
= menu
->h
;
XConfigureWindow(dpy
, menu
->win
, CWWidth
| CWHeight
| CWX
| CWY
, &changes
);
/* set window title (used if wflag is on) */
if (menu
->parent
== NULL
) {
} else if (menu
->caller
->output
) {
title
= menu
->caller
->output
;
XStringListToTextProperty(&title
, 1, &wintitle
);
/* set window manager hints */
sizeh
.flags
= USPosition
| PMaxSize
| PMinSize
;
sizeh
.min_width
= sizeh
.max_width
= menu
->w
;
sizeh
.min_height
= sizeh
.max_height
= menu
->h
;
XSetWMProperties(dpy
, menu
->win
, &wintitle
, NULL
, NULL
, 0, &sizeh
, NULL
, &classh
);
/* set WM protocols and ewmh window properties */
XSetWMProtocols(dpy
, menu
->win
, &wmdelete
, 1);
XChangeProperty(dpy
, menu
->win
, netatom
[NetWMName
], utf8string
, 8,
PropModeReplace
, (unsigned char *)title
, strlen(title
));
XChangeProperty(dpy
, menu
->win
, netatom
[NetWMWindowType
], XA_ATOM
, 32,
(unsigned char *)&netatom
[NetWMWindowTypePopupMenu
], 1);
/* calculate positions of submenus */
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
) {
if (item
->submenu
!= NULL
)
setupmenu(item
->submenu
, mon
);
/* try to grab pointer, we may have to wait for another process to ungrab */
struct timespec ts
= { .tv_sec
= 0, .tv_nsec
= 1000000 };
for (i
= 0; i
< 1000; i
++) {
if (XGrabPointer(dpy
, rootwin
, True
, ButtonPressMask
,
GrabModeAsync
, GrabModeAsync
, None
,
None
, CurrentTime
) == GrabSuccess
)
errx(1, "could not grab pointer");
/* try to grab keyboard, we may have to wait for another process to ungrab */
struct timespec ts
= { .tv_sec
= 0, .tv_nsec
= 1000000 };
for (i
= 0; i
< 1000; i
++) {
if (XGrabKeyboard(dpy
, rootwin
, True
, GrabModeAsync
,
GrabModeAsync
, CurrentTime
) == GrabSuccess
)
errx(1, "could not grab keyboard");
/* try to grab focus, we may have to wait for another process to ungrab */
struct timespec ts
= { .tv_sec
= 0, .tv_nsec
= 10000000 };
for (i
= 0; i
< 100; ++i
) {
XGetInputFocus(dpy
, &focuswin
, &revertwin
);
XSetInputFocus(dpy
, win
, RevertToParent
, CurrentTime
);
errx(1, "cannot grab focus");
/* ungrab pointer and keyboard */
XUngrabPointer(dpy
, CurrentTime
);
XUngrabKeyboard(dpy
, CurrentTime
);
/* check if path is absolute or relative to current directory */
isabsolute(const char *s
)
return s
[0] == '/' || (s
[0] == '.' && (s
[1] == '/' || (s
[1] == '.' && s
[2] == '/')));
/* load and scale icon */
loadicon(const char *file
)
Imlib_Load_Error errcode
;
warnx("could not load icon (file name is blank)");
icon
= imlib_load_image_with_error_return(file
, &errcode
);
for (i
= 0; i
< niconpaths
; i
++) {
snprintf(path
, sizeof(path
), "%s/%s", iconpaths
[i
], file
);
icon
= imlib_load_image_with_error_return(path
, &errcode
);
if (icon
!= NULL
|| errcode
!= IMLIB_LOAD_ERROR_FILE_DOES_NOT_EXIST
) {
case IMLIB_LOAD_ERROR_FILE_DOES_NOT_EXIST
:
errstr
= "file does not exist";
case IMLIB_LOAD_ERROR_FILE_IS_DIRECTORY
:
errstr
= "file is directory";
case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_READ
:
case IMLIB_LOAD_ERROR_PERMISSION_DENIED_TO_WRITE
:
errstr
= "permission denied";
case IMLIB_LOAD_ERROR_NO_LOADER_FOR_FILE_FORMAT
:
errstr
= "unknown file format";
case IMLIB_LOAD_ERROR_PATH_TOO_LONG
:
errstr
= "path too long";
case IMLIB_LOAD_ERROR_PATH_COMPONENT_NON_EXISTANT
:
case IMLIB_LOAD_ERROR_PATH_COMPONENT_NOT_DIRECTORY
:
case IMLIB_LOAD_ERROR_PATH_POINTS_OUTSIDE_ADDRESS_SPACE
:
errstr
= "improper path";
case IMLIB_LOAD_ERROR_TOO_MANY_SYMBOLIC_LINKS
:
errstr
= "too many symbolic links";
case IMLIB_LOAD_ERROR_OUT_OF_MEMORY
:
errstr
= "out of memory";
case IMLIB_LOAD_ERROR_OUT_OF_FILE_DESCRIPTORS
:
errstr
= "out of file descriptors";
errstr
= "unknown error";
warnx("could not load icon (%s): %s", errstr
, file
);
imlib_context_set_image(icon
);
width
= imlib_image_get_width();
height
= imlib_image_get_height();
imgsize
= min(width
, height
);
icon
= imlib_create_cropped_scaled_image(0, 0, imgsize
, imgsize
,
/* draw pixmap for the selected and unselected version of each item on menu */
drawitems(struct Menu
*menu
)
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
) {
item
->unsel
= XCreatePixmap(dpy
, menu
->win
, menu
->w
, item
->h
, depth
);
XSetForeground(dpy
, dc
.gc
, dc
.normal
[ColorBG
].pixel
);
XFillRectangle(dpy
, item
->unsel
, dc
.gc
, 0, 0, menu
->w
, item
->h
);
if (item
->label
== NULL
) { /* item is separator */
XSetForeground(dpy
, dc
.gc
, dc
.separator
.pixel
);
XDrawLine(dpy
, item
->unsel
, dc
.gc
, config
.horzpadding
, y
,
menu
->w
- config
.horzpadding
, y
);
item
->sel
= XCreatePixmap(dpy
, menu
->win
, menu
->w
, item
->h
, depth
);
XSetForeground(dpy
, dc
.gc
, dc
.selected
[ColorBG
].pixel
);
XFillRectangle(dpy
, item
->sel
, dc
.gc
, 0, 0, menu
->w
, item
->h
);
textx
= config
.horzpadding
;
textx
+= (iflag
|| !menu
->hasicon
) ? 0 : config
.horzpadding
+ config
.iconsize
;
switch (config
.alignment
) {
textx
+= (menu
->maxtextw
- item
->textw
) / 2;
textx
+= menu
->maxtextw
- item
->textw
;
dsel
= XftDrawCreate(dpy
, item
->sel
, visual
, colormap
);
dunsel
= XftDrawCreate(dpy
, item
->unsel
, visual
, colormap
);
XSetForeground(dpy
, dc
.gc
, dc
.selected
[ColorFG
].pixel
);
drawtext(dsel
, &dc
.selected
[ColorFG
], textx
, 0, item
->h
, item
->label
);
XSetForeground(dpy
, dc
.gc
, dc
.normal
[ColorFG
].pixel
);
drawtext(dunsel
, &dc
.normal
[ColorFG
], textx
, 0, item
->h
, item
->label
);
if (item
->submenu
!= NULL
) {
x
= menu
->w
- config
.triangle_width
- config
.horzpadding
;
y
= (item
->h
- config
.triangle_height
+ 1) / 2;
{x
+ config
.triangle_width
, y
+ config
.triangle_height
/2},
{x
, y
+ config
.triangle_height
},
XSetForeground(dpy
, dc
.gc
, dc
.selected
[ColorFG
].pixel
);
XFillPolygon(dpy
, item
->sel
, dc
.gc
, triangle
, LEN(triangle
),
Convex
, CoordModeOrigin
);
XSetForeground(dpy
, dc
.gc
, dc
.normal
[ColorFG
].pixel
);
XFillPolygon(dpy
, item
->unsel
, dc
.gc
, triangle
, LEN(triangle
),
Convex
, CoordModeOrigin
);
if (item
->file
&& !iflag
) {
item
->icon
= loadicon(item
->file
);
/* draw icon if properly loaded */
imlib_context_set_image(item
->icon
);
imlib_context_set_drawable(item
->sel
);
imlib_render_image_on_drawable(config
.horzpadding
, config
.iconpadding
);
imlib_context_set_drawable(item
->unsel
);
imlib_render_image_on_drawable(config
.horzpadding
, config
.iconpadding
);
imlib_context_set_image(item
->icon
);
/* copy pixmaps of items of the current menu and of its ancestors into menu window */
drawmenus(struct Menu
*currmenu
)
for (menu
= currmenu
; menu
!= NULL
; menu
= menu
->parent
) {
if (menu
->overflow
&& menu
->selected
!= NULL
) {
maxh
= menu
->h
- config
.height_pixels
* 2;
while (menu
->first
->next
!= NULL
&&
menu
->selected
->y
>= menu
->first
->y
+ maxh
) {
menu
->first
= menu
->first
->next
;
while (menu
->first
->prev
!= NULL
&&
menu
->selected
->y
< menu
->first
->y
) {
menu
->first
= menu
->first
->prev
;
y0
= menu
->overflow
? config
.height_pixels
: 0;
for (item
= menu
->first
; item
!= NULL
; item
= item
->next
) {
if (menu
->overflow
&& item
->y
- y
+ item
->h
> menu
->h
- config
.height_pixels
* 2)
if (item
== menu
->selected
) {
XCopyArea(dpy
, item
->sel
, menu
->win
, dc
.gc
, 0, 0, menu
->w
, item
->h
, 0, y0
+ item
->y
- y
);
XCopyArea(dpy
, item
->unsel
, menu
->win
, dc
.gc
, 0, 0, menu
->w
, item
->h
, 0, y0
+ item
->y
- y
);
/* unmap current menu and its parents */
unmapmenu(struct Menu
*currmenu
)
for (menu
= currmenu
; menu
!= NULL
; menu
= menu
->parent
) {
XUnmapWindow(dpy
, menu
->win
);
/* umap previous menus and map current menu and its parents */
mapmenu(struct Menu
*currmenu
, struct Menu
*prevmenu
, struct Monitor
*mon
)
struct Menu
*menu
, *menu_
;
struct Menu
*lcamenu
; /* lowest common ancestor menu */
int minlevel
; /* level of the closest to root menu */
int maxlevel
; /* level of the closest to root menu */
/* do not remap current menu if it wasn't updated*/
if (prevmenu
== currmenu
)
/* if this is the first time mapping, skip calculations */
XMapWindow(dpy
, currmenu
->win
);
/* find lowest common ancestor menu */
minlevel
= min(currmenu
->level
, prevmenu
->level
);
maxlevel
= max(currmenu
->level
, prevmenu
->level
);
if (currmenu
->level
== maxlevel
) {
while (menu
->level
> minlevel
)
/* unmap menus from currmenu (inclusive) until lcamenu (exclusive) */
for (menu
= prevmenu
; menu
!= lcamenu
; menu
= menu
->parent
) {
XUnmapWindow(dpy
, menu
->win
);
/* map menus from currmenu (inclusive) until lcamenu (exclusive) */
for (menu
= currmenu
; menu
!= lcamenu
; menu
= menu
->parent
) {
XMoveWindow(dpy
, menu
->win
, menu
->x
, menu
->y
);
XMapWindow(dpy
, menu
->win
);
grabfocus(currmenu
->win
);
/* get menu of given window */
getmenu(struct Menu
*currmenu
, Window win
)
for (menu
= currmenu
; menu
!= NULL
; menu
= menu
->parent
)
/* get in *ret the item in given menu and position; return 1 if position is on a scroll triangle */
getitem(struct Menu
*menu
, struct Item
**ret
, int y
)
if (y
< config
.height_pixels
) {
*ret
= menu
->first
->prev
;
} else if (y
> menu
->h
- config
.height_pixels
) {
y0
= menu
->overflow
? config
.height_pixels
: 0;
y
= menu
->h
- y0
+ menu
->first
->y
;
for (item
= menu
->first
; item
!= NULL
; item
= item
->next
)
if (y
>= item
->y
&& y
<= item
->y
+ item
->h
)
*ret
= menu
->first
->next
;
y0
= menu
->overflow
? config
.height_pixels
: 0;
y
-= y0
- menu
->first
->y
;
for (item
= menu
->first
; item
!= NULL
; item
= item
->next
) {
if (y
>= item
->y
&& y
<= item
->y
+ item
->h
) {
/* cycle through the items; non-zero direction is next, zero is prev */
itemcycle(struct Menu
*currmenu
, int direction
)
struct Item
*item
= NULL
;
for (lastitem
= currmenu
->list
; lastitem
&& lastitem
->next
; lastitem
= lastitem
->next
)
/* select item (either separator or labeled item) in given direction */
if (currmenu
->selected
== NULL
)
else if (currmenu
->selected
->next
!= NULL
)
item
= currmenu
->selected
->next
;
if (currmenu
->selected
== NULL
)
else if (currmenu
->selected
->prev
!= NULL
)
item
= currmenu
->selected
->prev
;
* the selected item can be a separator
* let's select the closest labeled item (ie., one that isn't a separator)
while (item
!= NULL
&& item
->label
== NULL
)
while (item
!= NULL
&& item
->label
== NULL
)
/* check if button is used to scroll */
isscrollbutton(unsigned int button
)
if (button
== Button4
|| button
== Button5
)
/* check if button is used to open a item on click */
isclickbutton(unsigned int button
)
if (button
== Button1
|| button
== Button2
)
if (!rflag
&& button
== Button3
)
/* warp pointer to center of selected item */
warppointer(struct Menu
*menu
, struct Item
*item
)
if (menu
== NULL
|| item
== NULL
)
XWarpPointer(dpy
, None
, menu
->win
, 0, 0, 0, 0, menu
->w
/ 2, item
->y
+ item
->h
/ 2);
/* append buf into text */
append(char *text
, char *buf
, size_t textsize
, size_t buflen
)
if (textlen
+ buflen
> textsize
- 1)
memcpy(text
+ textlen
, buf
, buflen
);
text
[textlen
+ buflen
] = '\0';
/* get item in menu matching text from given direction (or from beginning, if dir = 0) */
matchitem(struct Menu
*menu
, char *text
, int dir
)
struct Item
*item
, *lastitem
;
for (lastitem
= menu
->list
; lastitem
&& lastitem
->next
; lastitem
= lastitem
->next
)
if (menu
->selected
&& menu
->selected
->prev
)
item
= menu
->selected
->prev
;
if (menu
->selected
&& menu
->selected
->next
)
item
= menu
->selected
->next
;
/* find next item from selected item */
for ( ; item
; item
= (dir
< 0) ? item
->prev
: item
->next
)
for (s
= item
->label
; s
&& *s
; s
++)
if (strncasecmp(s
, text
, textlen
) == 0)
/* if not found, try to find from the beginning/end of list */
for (item
= menu
->list
; item
; item
= item
->next
) {
for (s
= item
->label
; s
&& *s
; s
++) {
if (strncasecmp(s
, text
, textlen
) == 0) {
for (item
= lastitem
; item
; item
= item
->prev
) {
for (s
= item
->label
; s
&& *s
; s
++) {
if (strncasecmp(s
, text
, textlen
) == 0) {
/* check keysyms defined on config.h */
normalizeksym(KeySym ksym
)
run(struct Menu
*currmenu
, struct Monitor
*mon
)
struct Menu
*menu
, *prevmenu
;
struct Item
*previtem
= NULL
;
struct Item
*lastitem
, *select
;
while (!XNextEvent(dpy
, &ev
)) {
if (XFilterEvent(&ev
, None
))
if (ev
.xexpose
.count
== 0)
menu
= getmenu(currmenu
, ev
.xbutton
.window
);
if (getitem(menu
, &item
, ev
.xbutton
.y
))
if (menu
== NULL
|| item
== NULL
|| previtem
== item
)
select
= menu
->selected
= item
;
if (item
->submenu
!= NULL
) {
currmenu
= item
->submenu
;
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_MAP
| ACTION_DRAW
;
if (isscrollbutton(ev
.xbutton
.button
)) {
if (ev
.xbutton
.button
== Button4
)
select
= itemcycle(currmenu
, ITEMPREV
);
select
= itemcycle(currmenu
, ITEMNEXT
);
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
| ACTION_WARP
;
} else if (isclickbutton(ev
.xbutton
.button
)) {
menu
= getmenu(currmenu
, ev
.xbutton
.window
);
if (getitem(menu
, &item
, ev
.xbutton
.y
) && item
!= NULL
) {
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_MAP
| ACTION_DRAW
;
if (menu
== NULL
|| item
== NULL
)
break; /* ignore separators */
if (item
->submenu
!= NULL
) {
currmenu
= item
->submenu
;
printf("%s\n", item
->output
);
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_MAP
| ACTION_DRAW
;
if (ev
.xbutton
.button
== Button2
) {
menu
= getmenu(currmenu
, ev
.xbutton
.window
);
len
= XmbLookupString(currmenu
->xic
, &ev
.xkey
, buf
, sizeof buf
, &ksym
, &status
);
default: /* XLookupNone, XBufferOverflow */
case XLookupKeySym
: /* FALLTHROUGH */
/* esc closes xmenu when current menu is the root menu */
if (ksym
== XK_Escape
&& currmenu
->parent
== NULL
)
/* Shift-Tab = ISO_Left_Tab */
if (ksym
== XK_Tab
&& (ev
.xkey
.state
& ShiftMask
))
ksym
= normalizeksym(ksym
);
select
= itemcycle(currmenu
, ITEMFIRST
);
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
;
select
= itemcycle(currmenu
, ITEMLAST
);
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
;
select
= matchitem(currmenu
, text
, -1);
action
= ACTION_SELECT
| ACTION_DRAW
;
select
= itemcycle(currmenu
, ITEMPREV
);
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
;
select
= matchitem(currmenu
, text
, 1);
action
= ACTION_SELECT
| ACTION_DRAW
;
select
= itemcycle(currmenu
, ITEMNEXT
);
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
;
case XK_1
: case XK_2
: case XK_3
: case XK_4
: case XK_5
: case XK_6
: case XK_7
: case XK_8
: case XK_9
:
item
= itemcycle(currmenu
, ITEMFIRST
);
lastitem
= itemcycle(currmenu
, ITEMLAST
);
for (int i
= ksym
- XK_1
; i
> 0 && item
!= lastitem
; i
--) {
currmenu
->selected
= item
;
item
= itemcycle(currmenu
, ITEMNEXT
);
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
;
case XK_Return
: case XK_Right
:
if (currmenu
->selected
) {
item
= currmenu
->selected
;
case XK_Escape
: case XK_Left
:
select
= currmenu
->parent
->selected
;
currmenu
= currmenu
->parent
;
action
= ACTION_CLEAR
| ACTION_MAP
| ACTION_SELECT
| ACTION_DRAW
;
case XK_BackSpace
: case XK_Clear
: case XK_Delete
:
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
;
if (*buf
== '\0' || iscntrl(*buf
))
for (i
= 0; i
< 2; i
++) {
append(text
, buf
, sizeof text
, len
);
if ((select
= matchitem(currmenu
, text
, 0)))
action
= ACTION_SELECT
| ACTION_DRAW
;
action
= ACTION_CLEAR
| ACTION_SELECT
| ACTION_DRAW
;
menu
= getmenu(currmenu
, ev
.xconfigure
.window
);
menu
->x
= ev
.xconfigure
.x
;
menu
->y
= ev
.xconfigure
.y
;
if ((unsigned long)ev
.xclient
.data
.l
[0] != wmdelete
)
menu
= getmenu(currmenu
, ev
.xclient
.window
);
if (menu
->parent
== NULL
)
goto done
; /* closing the root menu closes the program */
if (action
& ACTION_CLEAR
)
if (action
& ACTION_SELECT
)
currmenu
->selected
= select
;
prevmenu
= mapmenu(currmenu
, prevmenu
, mon
);
if (action
& ACTION_DRAW
)
if (action
& ACTION_WARP
) {
warppointer(currmenu
, select
);
/* recursivelly free pixmaps and destroy windows */
cleanmenu(struct Menu
*menu
)
if (item
->submenu
!= NULL
)
cleanmenu(item
->submenu
);
XFreePixmap(dpy
, item
->unsel
);
XFreePixmap(dpy
, item
->sel
);
if (tmp
->label
!= tmp
->output
)
XDestroyWindow(dpy
, menu
->win
);
/* cleanup draw context */
XftColorFree(dpy
, visual
, colormap
, &dc
.normal
[ColorBG
]);
XftColorFree(dpy
, visual
, colormap
, &dc
.normal
[ColorFG
]);
XftColorFree(dpy
, visual
, colormap
, &dc
.selected
[ColorBG
]);
XftColorFree(dpy
, visual
, colormap
, &dc
.selected
[ColorFG
]);
XftColorFree(dpy
, visual
, colormap
, &dc
.separator
);
XftColorFree(dpy
, visual
, colormap
, &dc
.border
);
for (i
= 0; i
< dc
.nfonts
; i
++)
XftFontClose(dpy
, dc
.fonts
[i
]);
/* xmenu: generate menu from stdin and print selected entry to stdout */
main(int argc
, char *argv
[])
/* open connection to server and set X variables */
if (!setlocale(LC_CTYPE
, "") || !XSupportsLocale())
warnx("warning: no locale support");
if ((dpy
= XOpenDisplay(NULL
)) == NULL
)
errx(1, "could not open display");
screen
= DefaultScreen(dpy
);
visual
= DefaultVisual(dpy
, screen
);
rootwin
= RootWindow(dpy
, screen
);
colormap
= DefaultColormap(dpy
, screen
);
depth
= DefaultDepth(dpy
, screen
);
if ((xrm
= XResourceManagerString(dpy
)) != NULL
)
xdb
= XrmGetStringDatabase(xrm
);
if ((xim
= XOpenIM(dpy
, NULL
, NULL
, NULL
)) == NULL
)
errx(1, "XOpenIM: could not open input device");
/* process configuration and window class */
imlib_set_cache_size(2048 * 1024);
imlib_context_set_dither(1);
imlib_context_set_display(dpy
);
imlib_context_set_visual(visual
);
imlib_context_set_colormap(colormap
);
/* if running in root mode, get button presses from root window */
XGrabButton(dpy
, button
, AnyModifier
, rootwin
, False
, ButtonPressMask
, GrabModeSync
, GrabModeSync
, None
, None
);
errx(1, "no menu generated");
(ev
.type
== ButtonPress
&&
((modifier
!= 0 && (ev
.xbutton
.state
& modifier
)) ||
(ev
.xbutton
.subwindow
== None
)))) {
if (rootmodeflag
&& passclickflag
) {
XAllowEvents(dpy
, ReplayPointer
, CurrentTime
);
setupmenu(rootmenu
, &mon
);
mapmenu(rootmenu
, NULL
, &mon
);
XAllowEvents(dpy
, ReplayPointer
, CurrentTime
);