#include <X11/Xresource.h>
#define LEN(x) (sizeof (x) / sizeof (x[0]))
#define MAX(x,y) ((x)>(y)?(x):(y))
#define MIN(x,y) ((x)<(y)?(x):(y))
enum {ColorFG
, ColorBG
, ColorLast
};
/* draw context structure */
XftColor normal
[ColorLast
];
XftColor selected
[ColorLast
];
XftColor decoration
[ColorLast
];
/* menu geometry structure */
int itemb
; /* item border */
int itemw
; /* item width */
int itemh
; /* item height */
int border
; /* window border width */
int separator
; /* menu separator width */
/* screen geometry structure */
int cursx
, cursy
; /* cursor position */
int screenw
, screenh
; /* screen width and height */
/* menu item structure */
char *label
; /* string to be drawed on menu */
char *output
; /* string to be outputed when item is clicked */
int y
; /* item y position relative to menu */
size_t labellen
; /* strlen(label) */
struct Item
*prev
; /* previous item */
struct Item
*next
; /* next item */
struct Menu
*submenu
; /* submenu spawned by clicking on item */
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
*selected
; /* item currently selected in the menu */
int x
, y
, w
, h
; /* menu geometry */
unsigned level
; /* menu level relative to root */
Drawable pixmap
; /* pixmap to draw the menu on */
Window win
; /* menu window to map on the screen */
/* function declarations */
static void getcolor(const char *s
, XftColor
*color
);
static void getresources(void);
static void setupdc(void);
static void setupgeom(void);
static struct Item
*allocitem(const char *label
, const char *output
);
static struct Menu
*allocmenu(struct Menu
*parent
, struct Item
*list
, unsigned level
);
static void getmenuitem(Window win
, int y
, struct Menu
**menu_ret
, struct Item
**item_ret
);
static void drawmenu(void);
static void calcscreengeom(void);
static void calcmenu(struct Menu
*menu
);
static void grabpointer(void);
static void grabkeyboard(void);
static void setcurrmenu(struct Menu
*currmenu_new
);
static void parsestdin(void);
static void freewindow(struct Menu
*menu
);
static void cleanup(void);
static Colormap colormap
;
static struct Menu
*rootmenu
= NULL
;
static struct Menu
*currmenu
= NULL
;
static int menutitlecount
;
static struct Geometry geom
;
static struct ScreenGeometry screengeom
;
main(int argc
, char *argv
[])
while ((ch
= getopt(argc
, argv
, "")) != -1) {
/* open connection to server and set X variables */
if ((dpy
= XOpenDisplay(NULL
)) == NULL
)
errx(1, "cannot open display");
screen
= DefaultScreen(dpy
);
visual
= DefaultVisual(dpy
, screen
);
rootwin
= RootWindow(dpy
, screen
);
colormap
= DefaultColormap(dpy
, screen
);
/* generate menus and recalculate them */
errx(1, "no menu generated");
/* grab mouse and keyboard */
XMapWindow(dpy
, rootmenu
->win
);
/* read xrdb for configuration options */
if ((xrm
= XResourceManagerString(dpy
))) {
xdb
= XrmGetStringDatabase(xrm
);
if (XrmGetResource(xdb
, "xmenu.menuborder", "*", &type
, &xval
) == True
)
if ((n
= strtol(xval
.addr
, NULL
, 10)) > 0)
if (XrmGetResource(xdb
, "xmenu.separatorsize", "*", &type
, &xval
) == True
)
if ((n
= strtol(xval
.addr
, NULL
, 10)) > 0)
if (XrmGetResource(xdb
, "xmenu.itemborder", "*", &type
, &xval
) == True
)
if ((n
= strtol(xval
.addr
, NULL
, 10)) > 0)
if (XrmGetResource(xdb
, "xmenu.width", "*", &type
, &xval
) == True
)
if ((n
= strtol(xval
.addr
, NULL
, 10)) > 0)
if (XrmGetResource(xdb
, "xmenu.background", "*", &type
, &xval
) == True
)
background
= strdup(xval
.addr
);
if (XrmGetResource(xdb
, "xmenu.foreground", "*", &type
, &xval
) == True
)
foreground
= strdup(xval
.addr
);
if (XrmGetResource(xdb
, "xmenu.selbackground", "*", &type
, &xval
) == True
)
selbackground
= strdup(xval
.addr
);
if (XrmGetResource(xdb
, "xmenu.selforeground", "*", &type
, &xval
) == True
)
selforeground
= strdup(xval
.addr
);
if (XrmGetResource(xdb
, "xmenu.separator", "*", &type
, &xval
) == True
)
separator
= strdup(xval
.addr
);
if (XrmGetResource(xdb
, "xmenu.border", "*", &type
, &xval
) == True
)
border
= strdup(xval
.addr
);
if (XrmGetResource(xdb
, "xmenu.font", "*", &type
, &xval
) == True
)
font
= strdup(xval
.addr
);
/* get color from color string */
getcolor(const char *s
, XftColor
*color
)
if(!XftColorAllocName(dpy
, visual
, colormap
, s
, color
))
errx(1, "cannot allocate color: %s", s
);
getcolor(background
, &dc
.normal
[ColorBG
]);
getcolor(foreground
, &dc
.normal
[ColorFG
]);
getcolor(selbackground
, &dc
.selected
[ColorBG
]);
getcolor(selforeground
, &dc
.selected
[ColorFG
]);
getcolor(separator
, &dc
.decoration
[ColorBG
]);
getcolor(border
, &dc
.decoration
[ColorFG
]);
if ((dc
.font
= XftFontOpenName(dpy
, screen
, font
)) == NULL
)
errx(1, "cannot load font");
dc
.gc
= XCreateGC(dpy
, rootwin
, 0, NULL
);
/* init menu geometry values */
geom
.itemh
= dc
.font
->height
+ itemborder
* 2;
geom
.border
= menuborder
;
geom
.separator
= separatorsize
;
allocitem(const char *label
, const char *output
)
if ((item
= malloc(sizeof *item
)) == NULL
)
if ((item
->label
= strdup(label
)) == NULL
)
if ((item
->output
= strdup(output
)) == NULL
)
item
->h
= item
->label
? geom
.itemh
: geom
.separator
;
item
->labellen
= strlen(item
->label
);
allocmenu(struct Menu
*parent
, struct Item
*list
, unsigned level
)
XSetWindowAttributes swa
;
if ((menu
= malloc(sizeof *menu
)) == NULL
)
menu
->h
= 0; /* calculated by calcmenu() */
menu
->x
= 0; /* calculated by calcmenu() */
menu
->y
= 0; /* calculated by calcmenu() */
swa
.override_redirect
= True
;
swa
.background_pixel
= dc
.decoration
[ColorBG
].pixel
;
swa
.border_pixel
= dc
.decoration
[ColorFG
].pixel
;
swa
.event_mask
= ExposureMask
| KeyPressMask
| ButtonPressMask
| ButtonReleaseMask
| PointerMotionMask
| LeaveWindowMask
;
menu
->win
= XCreateWindow(dpy
, rootwin
, 0, 0, geom
.itemw
, geom
.itemh
, geom
.border
,
CopyFromParent
, CopyFromParent
, CopyFromParent
,
CWOverrideRedirect
| CWBackPixel
| CWBorderPixel
| CWEventMask
,
/* create menus and items from the stdin */
struct Item
*curritem
= NULL
; /* item currently being read */
struct Menu
*prevmenu
= NULL
; /* menu the previous item was added to */
struct Item
*item
; /* dummy item for for loops */
struct Menu
*menu
; /* dummy menu for for loops */
size_t count
= 0; /* number of items in the current menu */
while (fgets(buf
, BUFSIZ
, stdin
) != NULL
) {
while (*s
!= '\0' && *s
!= '\t' && *s
!= '\n')
if (*s
!= '\0' && *s
!= '\n')
while (*s
!= '\0' && *s
!= '\n')
curritem
= allocitem(label
, output
);
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*/
for (menu
= prevmenu
, i
= level
;
menu
!= NULL
&& i
< prevmenu
->level
;
menu
= menu
->parent
, i
++)
errx(1, "reached NULL menu");
for (item
= menu
->list
; item
->next
!= NULL
; item
= item
->next
)
} else if (level
== prevmenu
->level
) { /* item is a continuation of current 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
);
for (item
= prevmenu
->list
; item
->next
!= NULL
; item
= item
->next
)
/* calculate screen geometry */
Window w1
, w2
; /* unused variables */
int a
, b
; /* unused variables */
unsigned mask
; /* unused variable */
XQueryPointer(dpy
, rootwin
, &w1
, &w2
, &screengeom
.cursx
, &screengeom
.cursy
, &a
, &b
, &mask
);
screengeom
.screenw
= DisplayWidth(dpy
, screen
);
screengeom
.screenh
= DisplayHeight(dpy
, screen
);
/* recursivelly calculate menu geometry and set window hints */
calcmenu(struct Menu
*menu
)
static XClassHint classh
= {PROGNAME
, PROGNAME
};
/* calculate items positions and menu width and height */
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
) {
if (item
->label
== NULL
) /* height for separator item */
menu
->h
+= geom
.separator
;
XftTextExtentsUtf8(dpy
, dc
.font
, (XftChar8
*)item
->label
,
labelwidth
= ext
.xOff
+ dc
.font
->height
* 2;
menu
->w
= MAX(menu
->w
, labelwidth
);
/* calculate menu's x and y positions */
if (menu
->parent
== NULL
) { /* if root menu, calculate in respect to cursor */
if (screengeom
.screenw
- screengeom
.cursx
>= menu
->w
)
menu
->x
= screengeom
.cursx
;
else if (screengeom
.cursx
> menu
->w
)
menu
->x
= screengeom
.cursx
- menu
->w
;
if (screengeom
.screenh
- screengeom
.cursy
>= menu
->h
)
menu
->y
= screengeom
.cursy
;
else if (screengeom
.screenh
> menu
->h
)
menu
->y
= screengeom
.screenh
- menu
->h
;
XStringListToTextProperty(menutitle
, menutitlecount
, &textprop
);
} else { /* else, calculate in respect to parent menu */
if (screengeom
.screenw
- (menu
->parent
->x
+ menu
->parent
->w
+ geom
.border
) >= menu
->w
)
menu
->x
= menu
->parent
->x
+ menu
->parent
->w
+ geom
.border
;
else if (menu
->parent
->x
> menu
->w
+ geom
.border
)
menu
->x
= menu
->parent
->x
- menu
->w
- geom
.border
;
if (screengeom
.screenh
- (menu
->caller
->y
+ menu
->parent
->y
) > menu
->h
)
menu
->y
= menu
->caller
->y
+ menu
->parent
->y
;
else if (screengeom
.screenh
- menu
->parent
->y
> menu
->h
)
menu
->y
= menu
->parent
->y
;
else if (screengeom
.screenh
> menu
->h
)
menu
->y
= screengeom
.screenh
- menu
->h
;
XStringListToTextProperty(&(menu
->caller
->output
), 1, &textprop
);
/* update menu geometry */
changes
.height
= menu
->h
;
XConfigureWindow(dpy
, menu
->win
, CWWidth
| CWHeight
| CWX
| CWY
, &changes
);
/* set window manager hints */
sizeh
.flags
= PMaxSize
| PMinSize
;
sizeh
.min_width
= sizeh
.max_width
= menu
->w
;
sizeh
.min_height
= sizeh
.max_height
= menu
->h
;
XSetWMProperties(dpy
, menu
->win
, &textprop
, NULL
, NULL
, 0, &sizeh
,
/* create pixmap and XftDraw */
menu
->pixmap
= XCreatePixmap(dpy
, menu
->win
, menu
->w
, menu
->h
,
DefaultDepth(dpy
, screen
));
menu
->draw
= XftDrawCreate(dpy
, menu
->pixmap
, visual
, colormap
);
/* calculate positions of submenus */
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
) {
if (item
->submenu
!= NULL
)
/* 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, "cannot grab keyboard");
/* 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, "cannot grab keyboard");
/* get menu and item of given window and position */
getmenuitem(Window win
, int y
,
struct Menu
**menu_ret
, struct Item
**item_ret
)
struct Menu
*menu
= NULL
;
struct Item
*item
= NULL
;
for (menu
= currmenu
; menu
!= NULL
; menu
= menu
->parent
) {
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
) {
if (y
>= item
->y
&& y
<= item
->y
+ item
->h
) {
/* set currentmenu to menu, umap previous menus and map current menu and its parents */
setcurrmenu(struct Menu
*currmenu_new
)
struct Menu
*menu
, *menu_
;
struct Menu
*lcamenu
; /* lowest common ancestor menu */
unsigned minlevel
; /* level of the closest to root menu */
unsigned maxlevel
; /* level of the closest to root menu */
/* do not update currmenu to itself */
if (currmenu_new
== currmenu
)
/* if there was no currmenu, skip calculations */
/* find lowest common ancestor menu */
minlevel
= MIN(currmenu_new
->level
, currmenu
->level
);
maxlevel
= MAX(currmenu_new
->level
, currmenu
->level
);
if (currmenu_new
->level
== maxlevel
) {
while (menu
->level
> minlevel
)
/* unmap menus from currmenu (inclusive) until lcamenu (exclusive) */
for (menu
= currmenu
; 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
) {
XMapWindow(dpy
, menu
->win
);
/* draw items of the current menu and of its ancestors */
for (menu
= currmenu
; menu
!= NULL
; menu
= menu
->parent
) {
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
) {
/* determine item color */
if (item
== menu
->selected
)
/* continue if item is a separator */
XSetForeground(dpy
, dc
.gc
, color
[ColorBG
].pixel
);
XDrawRectangle(dpy
, menu
->pixmap
, dc
.gc
, 0, item
->y
,
XFillRectangle(dpy
, menu
->pixmap
, dc
.gc
, 0, item
->y
,
labelx
= 0 + dc
.font
->height
;
labely
= item
->y
+ dc
.font
->height
+ geom
.itemb
/ 2;
XSetForeground(dpy
, dc
.gc
, color
[ColorFG
].pixel
);
XftDrawStringUtf8(menu
->draw
, &color
[ColorFG
], dc
.font
,
labelx
, labely
, item
->label
,
/* draw triangle, if item contains a submenu */
if (item
->submenu
!= NULL
) {
int trianglex
= menu
->w
- dc
.font
->height
+ geom
.itemb
- 1;
int triangley
= item
->y
+ (3 * item
->h
)/8 -1;
{trianglex
+ item
->h
/8 + 1, item
->y
+ item
->h
/2},
{trianglex
, triangley
+ item
->h
/4 + 2},
XFillPolygon(dpy
, menu
->pixmap
, dc
.gc
, triangle
, LEN(triangle
),
Convex
, CoordModeOrigin
);
XCopyArea(dpy
, menu
->pixmap
, menu
->win
, dc
.gc
, 0, item
->y
,
menu
->w
, item
->h
, 0, item
->y
);
/* cycle through the items; non-zero direction is next, zero is prev */
if (direction
== ITEMNEXT
) {
if (currmenu
->selected
== NULL
)
else if (currmenu
->selected
->next
!= NULL
)
item
= currmenu
->selected
->next
;
while (item
!= NULL
&& item
->label
== NULL
)
for (lastitem
= currmenu
->list
;
lastitem
!= NULL
&& lastitem
->next
!= NULL
;
lastitem
= lastitem
->next
)
if (currmenu
->selected
== NULL
)
else if (currmenu
->selected
->prev
!= NULL
)
item
= currmenu
->selected
->prev
;
while (item
!= NULL
&& item
->label
== NULL
)
struct Item
*previtem
= NULL
;
while (!XNextEvent(dpy
, &ev
)) {
if (ev
.xexpose
.count
== 0)
getmenuitem(ev
.xbutton
.window
, ev
.xbutton
.y
, &menu
, &item
);
if (menu
!= NULL
&& item
!= NULL
) {
if (item
->submenu
!= NULL
)
setcurrmenu(item
->submenu
);
} else if (menu
->selected
!= item
) {
getmenuitem(ev
.xbutton
.window
, ev
.xbutton
.y
, &menu
, &item
);
if (menu
!= NULL
&& item
!= NULL
) {
break; /* ignore separators */
if (item
->submenu
!= NULL
) {
setcurrmenu(item
->submenu
);
printf("%s\n", item
->output
);
currmenu
->selected
= currmenu
->list
;
getmenuitem(ev
.xbutton
.window
, ev
.xbutton
.y
, &menu
, &item
);
if (menu
== NULL
|| item
== NULL
)
ksym
= XkbKeycodeToKeysym(dpy
, ev
.xkey
.keycode
, 0, 0);
if (ksym
== XK_Escape
&& currmenu
== rootmenu
)
/* Shift-Tab = ISO_Left_Tab */
if (ksym
== XK_Tab
&& (ev
.xkey
.state
& ShiftMask
))
if (ksym
== XK_ISO_Left_Tab
|| ksym
== XK_Up
) {
item
= itemcycle(ITEMPREV
);
} else if (ksym
== XK_Tab
|| ksym
== XK_Down
) {
item
= itemcycle(ITEMNEXT
);
} else if ((ksym
== XK_Return
|| ksym
== XK_Right
) &&
currmenu
->selected
!= NULL
) {
item
= currmenu
->selected
;
} else if ((ksym
== XK_Escape
|| ksym
== XK_Left
) &&
currmenu
->parent
!= NULL
) {
item
= currmenu
->parent
->selected
;
setcurrmenu(currmenu
->parent
);
currmenu
->selected
= item
;
currmenu
->selected
= NULL
;
/* recursivelly free a pixmap */
freewindow(struct Menu
*menu
)
for (item
= menu
->list
; item
!= NULL
; item
= item
->next
)
if (item
->submenu
!= NULL
)
freewindow(item
->submenu
);
XFreePixmap(dpy
, menu
->pixmap
);
XftDrawDestroy(menu
->draw
);
XDestroyWindow(dpy
, menu
->win
);
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
.decoration
[ColorBG
]);
XftColorFree(dpy
, visual
, colormap
, &dc
.decoration
[ColorFG
]);
(void)fprintf(stderr
, "usage: xmenu title...\n");