Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
2664 lines (2352 sloc) 60.1 KB
/*
* jgmenu.c
*
* Copyright (C) Johan Malm 2014
*
* jgmenu is a stand-alone menu which reads the menu items from a CSV file
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/Xresource.h>
#include <X11/Xlocale.h>
#include <pthread.h>
#include <sys/select.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#include <math.h>
#include <sys/stat.h>
#include <sys/time.h>
#include "x11-ui.h"
#include "config.h"
#include "util.h"
#include "geometry.h"
#include "sbuf.h"
#include "icon.h"
#include "filter.h"
#include "list.h"
#include "lockfile.h"
#include "argv-buf.h"
#include "t2conf.h"
#include "ipc.h"
#include "bl.h"
#include "xsettings-helper.h"
#include "terminal.h"
#include "restart.h"
#include "theme.h"
#include "font.h"
#include "args.h"
#include "widgets.h"
#include "pm.h"
#include "workarea.h"
#include "charset.h"
#include "watch.h"
#include "spawn.h"
#define DEBUG_ICONS_LOADED_NOTIFICATION 0
static pthread_t thread; /* worker thread for loading icons */
static int pipe_fds[2]; /* talk between threads + catch sig */
static int sw_close_pending;
static int menu_is_hidden;
static int super_key_pressed;
struct item {
char *buf;
char *name;
char *cmd;
char *iconname;
char *working_dir;
char *metadata;
/* int strict_xdg_exec */
/* int start_notify; */
char *tag;
struct area area;
cairo_surface_t *icon;
int selectable;
struct list_head master;
struct list_head filter;
};
static struct item empty_item;
/* A node is marked by a ^tag() and denotes the start of a submenu */
struct node {
struct item *item; /* item that node points to */
struct item *last_sel; /* used when returning to node */
struct item *last_first; /* used when returning to node */
struct item *expanded; /* tracks item with sub window open */
struct node *parent;
Window wid;
struct list_head node;
};
/*
* The lists "master" and "filter" are the core building blocks of the code
* When a submenu is checked out, *subhead and *subtail are set.
*/
struct menu {
struct item *subhead; /* first item in checked out submenu */
struct item *subtail; /* last item in checked out submenu */
struct item *first; /* first visible item */
struct item *last; /* last visible item */
struct item *sel; /* currently selected item */
struct list_head master; /* all items */
struct list_head filter; /* items to be displayed in menu */
struct list_head nodes; /* hierarchical structure of tags */
struct node *current_node;
};
static struct menu menu;
static const char jgmenu_usage[] =
"Usage: jgmenu [command] [options]\n\n"
"Commands include init\n"
"Options:\n"
" --version show version\n"
" --no-spawn redirect command to stdout instead of executing\n"
" --checkout=<tag> checkout submenu <tag> on startup\n"
" --config-file=<file> specify config file\n"
" --icon-size=<size> specify icon size (22 by default); set to 0 to\n"
" disable icons\n"
" --at-pointer launch menu at mouse pointer\n"
" --hide-on-startup start menu is hidden state\n"
" --simple ignore tint2 settings and run in 'short-lived' mode\n"
" (i.e. exit after mouse click or enter/escape)\n"
" --vsimple same as --simple, but also disables icons and\n"
" ignores jgmenurc\n"
" --csv-file=<file> specify menu file (in jgmenu flavoured CSV format)\n"
" --csv-cmd=<command> specify command to producue menu data\n";
static void checkout_rootnode(void);
static void pipemenu_del_all(void);
static void pipemenu_del_beyond(struct node *node);
static void tmr_mouseover_stop(void);
static void del_beyond_current(void);
static void del_beyond_root(void);
static void init_empty_item(void)
{
empty_item.name = xstrdup("&lt;empty&gt;");
empty_item.cmd = xstrdup(":");
empty_item.iconname = NULL;
empty_item.working_dir = NULL;
empty_item.metadata = NULL;
empty_item.tag = NULL;
empty_item.icon = NULL;
empty_item.selectable = 1;
empty_item.area.h = config.item_height;
}
static void delete_empty_item(void)
{
xfree(empty_item.name);
xfree(empty_item.cmd);
}
static void usage(void)
{
printf("%s", jgmenu_usage);
exit(0);
}
static void version(void)
{
printf("%s\n", (char *)VERSION);
exit(0);
}
static void indent(int number_of_spaces)
{
int i;
if (number_of_spaces <= 0)
return;
for (i = 0; i < number_of_spaces; i++)
fprintf(stderr, " ");
}
static int level(struct node *n)
{
int i = 0;
while (n->parent) {
n = n->parent;
++i;
}
return i;
}
static void print_nodes(void)
{
struct node *n;
fprintf(stderr, "nodes:\n");
list_for_each_entry(n, &menu.nodes, node) {
indent(level(n) * 2);
fprintf(stderr, "%.8s ", n->item->tag);
if (pm_is_pipe_node(n))
fprintf(stderr, "[pipe] ");
if (n->expanded)
fprintf(stderr, "[expanded] ");
fprintf(stderr, "\n");
}
}
static struct item *filter_head(void)
{
return list_first_entry_or_null(&menu.filter, struct item, filter);
}
static struct item *filter_tail(void)
{
return list_last_entry(&menu.filter, struct item, filter);
}
static void step_back(struct item **ptr, int nr)
{
int i;
for (i = 0; i < nr; i++)
*ptr = container_of((*ptr)->filter.prev, struct item, filter);
}
static void step_fwd(struct item **ptr, int nr)
{
int i;
for (i = 0; i < nr; i++)
*ptr = container_of((*ptr)->filter.next, struct item, filter);
}
static struct item *fill_from_top(struct item *first)
{
struct item *p, *last;
int col, h;
h = geo_get_itemarea_height() - config.item_margin_y;
col = 1;
p = first;
last = p;
list_for_each_entry_from(p, &menu.filter, filter) {
h -= p->area.h + config.item_margin_y;
if (h < 0) {
col++;
h = geo_get_itemarea_height() - config.item_margin_y;
h -= p->area.h + config.item_margin_y;
}
if (col > config.columns)
break;
last = p;
}
return last;
}
static struct item *fill_from_bottom(struct item *last)
{
struct item *p, *first;
int col, h;
int ignoring = 1;
h = geo_get_itemarea_height() - config.item_margin_y;
col = 1;
first = last;
list_for_each_entry_reverse(p, &menu.filter, filter) {
if (p == last)
ignoring = 0;
if (ignoring)
continue;
h -= p->area.h + config.item_margin_y;
if (h < 0) {
col++;
h = geo_get_itemarea_height() - config.item_margin_y;
h -= p->area.h + config.item_margin_y;
}
if (col > config.columns)
break;
first = p;
}
return first;
}
static struct item *next_selectable(struct item *cur, int *isoutside)
{
struct item *p = cur;
*isoutside = 0;
while (p != filter_tail()) {
if (p == menu.last)
*isoutside = 1;
step_fwd(&p, 1);
if (p->selectable)
break;
}
if (!p->selectable)
p = cur;
return p;
}
static struct item *prev_selectable(struct item *cur, int *isoutside)
{
struct item *p = cur;
*isoutside = 0;
while (p != filter_head()) {
if (p == menu.first)
*isoutside = 1;
step_back(&p, 1);
if (p->selectable)
break;
}
if (!p->selectable)
p = cur;
return p;
}
static struct item *first_selectable(void)
{
int isoutside;
struct item *item;
item = filter_head();
if (!item->selectable)
item = next_selectable(item, &isoutside);
return item;
}
static struct item *last_selectable(void)
{
int isoutside;
struct item *item;
item = filter_tail();
if (!item->selectable)
item = prev_selectable(item, &isoutside);
return item;
}
static void add_if_unique(struct item *item)
{
struct item *p;
if (!item->cmd)
return;
list_for_each_entry(p, &menu.filter, filter) {
if (!p->cmd)
continue;
if (!strcmp(item->cmd, p->cmd))
return;
}
list_add_tail(&item->filter, &menu.filter);
}
static int isvisible(struct item *item)
{
struct item *p;
if (!item)
return 0;
p = menu.first;
list_for_each_entry_from(p, &menu.filter, filter) {
if (p == item)
return 1;
if (p == menu.last)
break;
}
return 0;
}
static void update_filtered_list(void)
{
struct item *item;
int isoutside;
INIT_LIST_HEAD(&menu.filter);
if (filter_needle_length()) {
del_beyond_root();
list_for_each_entry(item, &menu.master, master) {
if (!strncmp("^checkout(", item->cmd, 10) ||
!strncmp("^tag(", item->cmd, 5) ||
!strncmp("^pipe(", item->cmd, 6) ||
!strncmp("^back(", item->cmd, 6) ||
!strncmp("^sep(", item->cmd, 5))
continue;
if (filter_ismatch(item->name) ||
filter_ismatch(item->cmd) ||
filter_ismatch(item->metadata))
add_if_unique(item);
}
} else {
list_for_each_entry(item, &menu.master, master)
if (item == menu.subhead)
break;
list_for_each_entry_from(item, &menu.master, master) {
list_add_tail(&item->filter, &menu.filter);
if (item == menu.subtail)
break;
}
}
if (!filter_head()) {
list_add_tail(&empty_item.filter, &menu.filter);
menu.first = &empty_item;
menu.last = &empty_item;
menu.sel = &empty_item;
return;
}
if (!filter_needle_length() && menu.current_node->last_first)
menu.first = menu.current_node->last_first;
else
menu.first = filter_head();
menu.last = fill_from_top(menu.first);
/* select an item */
if (!filter_needle_length() && menu.current_node->last_sel) {
menu.sel = menu.current_node->last_sel;
if (!isvisible(menu.sel))
menu.sel = first_selectable();
} else {
menu.sel = menu.first;
if (!menu.sel->selectable)
menu.sel = next_selectable(menu.first, &isoutside);
menu.current_node->last_sel = menu.sel;
}
}
static void init_menuitem_coordinates(void)
{
struct item *p;
if (list_empty(&menu.filter))
return;
/* reset y variable */
geo_get_item_coordinates(NULL);
p = menu.first;
list_for_each_entry_from(p, &menu.filter, filter) {
geo_get_item_coordinates(&p->area);
if (p == menu.last)
break;
}
}
/*
* Returns bar from ^foo(bar)
* s="^foo(bar)"
* token="^foo("
*/
static char *parse_caret_action(char *s, char *token)
{
char *p;
p = NULL;
if (!s)
return NULL;
if (!strncmp(s, token, strlen(token))) {
p = s;
p += strlen(token);
}
return p;
}
static void draw_item_sep_without_text(struct item *p)
{
double y;
y = round(p->area.y + p->area.h / 2) + 0.5;
ui_draw_line(p->area.x + 5, y, p->area.x + p->area.w - 5, y,
1.0, config.color_sep_fg);
}
static void draw_item_sep_with_text(struct item *p)
{
struct sbuf s;
int text_x_coord;
sbuf_init(&s);
if (config.sep_markup && config.sep_markup[0] != '\0') {
sbuf_addstr(&s, "<span ");
sbuf_addstr(&s, config.sep_markup);
sbuf_addstr(&s, ">");
}
sbuf_addstr(&s, parse_caret_action(p->name, "^sep("));
if (config.sep_markup && config.sep_markup[0] != '\0')
sbuf_addstr(&s, "</span>");
text_x_coord = p->area.x;
if (config.sep_halign == LEFT)
text_x_coord += config.item_padding_x;
else if (config.sep_halign == RIGHT)
text_x_coord -= config.item_padding_x;
ui_draw_rectangle(p->area.x, p->area.y, p->area.w,
p->area.h, config.item_radius, 1.0, 1,
config.color_title_bg);
ui_draw_rectangle(p->area.x, p->area.y, p->area.w,
p->area.h, config.item_radius, 1.0, 0,
config.color_title_border);
ui_insert_text(s.buf, text_x_coord, p->area.y, p->area.h, p->area.w,
config.color_title_fg, config.sep_halign);
xfree(s.buf);
}
static void draw_item_sep(struct item *p)
{
if (p->name[strlen("^sep(")] == '\0')
draw_item_sep_without_text(p);
else
draw_item_sep_with_text(p);
}
static void draw_last_sel(struct item *p)
{
if (p == menu.sel)
return;
ui_draw_rectangle(p->area.x, p->area.y, p->area.w,
p->area.h, config.item_radius, 1.0,
0, config.color_sel_bg);
}
static void draw_item_bg_norm(struct item *p)
{
ui_draw_rectangle(p->area.x, p->area.y, p->area.w, p->area.h,
config.item_radius, config.item_border, 1,
config.color_norm_bg);
}
static void draw_item_bg_sel(struct item *p)
{
ui_draw_rectangle(p->area.x, p->area.y, p->area.w, p->area.h,
config.item_radius, config.item_border, 1,
config.color_sel_bg);
if (!config.item_border)
return;
ui_draw_rectangle(p->area.x, p->area.y, p->area.w, p->area.h,
config.item_radius, config.item_border, 0,
config.color_sel_border);
}
static void draw_item_text(struct item *p)
{
int text_x_coord, width;
if (config.item_halign != RIGHT) {
text_x_coord = p->area.x + config.item_padding_x;
if (config.icon_size)
text_x_coord += config.icon_size +
config.icon_text_spacing;
} else {
text_x_coord = p->area.x - config.item_padding_x;
}
width = p->area.w - config.item_padding_x;
if (config.icon_size)
width -= config.icon_size + config.icon_text_spacing;
if (p == menu.sel)
ui_insert_text(p->name, text_x_coord, p->area.y,
p->area.h, width, config.color_sel_fg,
config.item_halign);
else
ui_insert_text(p->name, text_x_coord, p->area.y,
p->area.h, width, config.color_norm_fg,
config.item_halign);
}
static void draw_submenu_arrow(struct item *p)
{
double *color;
color = (p == menu.sel) ? config.color_sel_fg : config.color_norm_fg;
if (config.item_halign != RIGHT)
ui_insert_text(config.arrow_string, p->area.x + p->area.w -
config.item_padding_x - (config.arrow_width * 0.7), p->area.y,
p->area.h, p->area.w, color,
config.item_halign);
else
ui_insert_text(config.arrow_string, p->area.x + config.item_padding_x,
p->area.y, p->area.h, config.arrow_width * 0.7,
color, config.item_halign);
}
static void draw_icon(struct item *p)
{
int icon_y_coord;
int offsety, offsetx;
offsety = cairo_image_surface_get_height(p->icon) < config.icon_size ?
(config.icon_size - cairo_image_surface_get_height(p->icon)) / 2 : 0;
offsetx = cairo_image_surface_get_width(p->icon) < config.icon_size ?
(config.icon_size - cairo_image_surface_get_width(p->icon)) / 2 : 0;
icon_y_coord = p->area.y + (config.item_height - config.icon_size) / 2 +
offsety;
if (config.item_halign != RIGHT)
ui_insert_image(p->icon, p->area.x + config.item_padding_x +
offsetx, icon_y_coord, config.icon_size);
else
ui_insert_image(p->icon, p->area.x + p->area.w - config.icon_size -
config.item_padding_x + offsetx - 1, icon_y_coord, config.icon_size);
}
static void draw_items_below_indicator(void)
{
int b, r, l;
if (!menu.current_node->parent) {
b = config.menu_padding_bottom;
r = config.menu_padding_right;
l = config.menu_padding_left;
} else {
b = config.sub_padding_bottom;
r = config.sub_padding_right;
l = config.sub_padding_left;
}
if (b < 1)
return;
ui_draw_line(l, geo_get_menu_height() - b - 0.5, geo_get_menu_width() - r,
geo_get_menu_height() - b - 0.5, 1.0, config.color_scroll_ind);
}
static void draw_items_above_indicator(void)
{
int t, r, l;
if (!menu.current_node->parent) {
t = config.menu_padding_top;
r = config.menu_padding_right;
l = config.menu_padding_left;
} else {
t = config.sub_padding_top;
r = config.sub_padding_right;
l = config.sub_padding_left;
}
if (t < 1)
return;
ui_draw_line(l, t + 0.5, geo_get_menu_width() - r, t + 0.5, 1.0,
config.color_scroll_ind);
}
static void draw_menu(void)
{
struct item *p;
int w;
w = geo_get_menu_width();
/* Draw background */
ui_clear_canvas();
ui_draw_rectangle(0, 0, w, geo_get_menu_height(), config.menu_radius,
config.menu_border, 1, config.color_menu_bg);
/* Draw menu border */
if (config.menu_border)
ui_draw_rectangle(0, 0, w, geo_get_menu_height(),
config.menu_radius, config.menu_border,
0, config.color_menu_border);
if (!ui->cur)
widgets_draw();
/* Draw menu items */
p = menu.first;
list_for_each_entry_from(p, &menu.filter, filter) {
/* Draw item background */
if (p == menu.sel)
draw_item_bg_sel(p);
else if (p->selectable)
draw_item_bg_norm(p);
if (p == menu.current_node->last_sel)
draw_last_sel(p);
/* Draw submenu arrow */
if (config.arrow_width && (!strncmp(p->cmd, "^checkout(", 10) ||
!strncmp(p->cmd, "^pipe(", 6) ||
!strncmp(p->cmd, "^root(", 6) ||
!strncmp(p->cmd, "^sub(", 5)))
draw_submenu_arrow(p);
/* Draw menu items text */
if (p->selectable)
draw_item_text(p);
else if (!strncmp(p->name, "^sep(", 5))
draw_item_sep(p);
/* Draw Icons */
if (config.icon_size && p->icon)
draw_icon(p);
if (p == menu.last)
break;
}
if (filter_tail() != menu.last)
draw_items_below_indicator();
if (filter_head() != menu.first)
draw_items_above_indicator();
ui_map_window(geo_get_menu_width(), geo_get_menu_height());
}
static struct node *get_node_from_tag(const char *tag)
{
struct node *n;
if (!tag)
return NULL;
list_for_each_entry(n, &menu.nodes, node) {
if (!strcmp(tag, n->item->tag))
return n;
}
return NULL;
}
static int submenu_itemarea_width(void)
{
struct item *p;
struct sbuf s;
struct point point;
sbuf_init(&s);
p = menu.subhead;
list_for_each_entry_from(p, &menu.master, master) {
if (p->name) {
sbuf_addstr(&s, p->name);
sbuf_addch(&s, '\n');
}
if (p == menu.subtail)
break;
}
point = ui_get_text_size(s.buf, font_get());
/* point.x now holds the width of the widest 'line of text' */
point.x += config.item_padding_x * 2;
point.x += config.item_margin_x * 2;
if (config.icon_size)
point.x += config.icon_size + config.icon_text_spacing;
point.x += config.arrow_width;
free(s.buf);
return point.x;
}
static void set_submenu_width(void)
{
int mw;
int reqw = geo_get_menu_width_from_itemarea_width(submenu_itemarea_width());
struct point maxarea = geo_get_max_menuarea_that_fits();
mw = reqw < maxarea.x ? reqw : maxarea.x;
if (mw < config.menu_width)
mw = config.menu_width;
if (config.position_mode != POSITION_MODE_PTR)
goto set_width;
/* grow from right hand edge if too near it */
if (config.menu_halign == LEFT && reqw > maxarea.x) {
geo_set_menu_margin_x(geo_get_screen_x0() +
geo_get_screen_width() -
reqw);
mw = reqw;
}
set_width:
geo_set_menu_width(mw);
}
static int submenu_itemarea_height(void)
{
struct item *p;
int h = 0;
p = menu.subhead;
list_for_each_entry_from(p, &menu.master, master) {
h += config.item_margin_y + p->area.h;
if (p == menu.subtail)
break;
}
h += config.item_margin_y;
return h;
}
static void set_submenu_height(void)
{
int reqh = submenu_itemarea_height();
struct point maxarea = geo_get_max_itemarea_that_fits();
int h;
h = reqh < maxarea.y ? reqh : maxarea.y;
geo_set_menu_height_from_itemarea_height(h);
}
static int tag_exists(const char *tag)
{
struct item *item;
if (!tag)
return 0;
list_for_each_entry(item, &menu.master, master)
if (item->tag && !strcmp(tag, item->tag))
return 1;
return 0;
}
static int tag_count(const char *tag)
{
struct item *item;
int n = 0;
if (!tag)
return 0;
list_for_each_entry(item, &menu.master, master)
if (item->tag && !strcmp(tag, item->tag))
++n;
return n;
}
static struct item *get_item_from_tag(const char *tag)
{
struct item *item;
BUG_ON(!tag);
list_for_each_entry(item, &menu.master, master)
if (item->tag && !strcmp(tag, item->tag))
return item;
if (tag && strncmp(tag, "root", 4))
fprintf(stderr, "warning: could not find tag '%s'\n", tag);
return NULL;
}
static char *tag_of_first_item(void)
{
struct item *item;
item = list_first_entry_or_null(&menu.master, struct item, master);
if (!item)
die("no items in master list");
return item->tag;
}
static void find_subhead(const char *tag)
{
BUG_ON(!tag);
if (tag_exists(tag)) {
menu.current_node = get_node_from_tag(tag);
} else {
warn("tag '%s' does not exist", tag);
menu.current_node = get_node_from_tag(tag_of_first_item());
}
if (!menu.current_node)
die("node '%s' does not exist", tag);
menu.subhead = container_of((menu.current_node->item)->master.next,
struct item, master);
if (!menu.subhead)
die("no menu.subhead");
}
static void find_subtail(void)
{
struct item *item;
menu.subtail = menu.subhead;
item = menu.subhead;
list_for_each_entry_from(item, &menu.master, master) {
if (item->tag)
break;
menu.subtail = item;
}
}
static void checkout_tag(const char *tag)
{
find_subhead(tag);
find_subtail();
}
static void checkout_submenu(char *tag)
{
if (!menu.sel) {
warn("checkout_submenu(): no menu.sel");
return;
}
if (geo_cur() >= MAX_NR_WINDOWS - 1) {
warn("Maximum number of windows reached ('%d')", MAX_NR_WINDOWS);
return;
}
checkout_tag(tag);
geo_win_add(menu.sel->area);
set_submenu_height();
set_submenu_width();
ui_win_add(geo_get_menu_x0(), geo_get_menu_y0(),
geo_get_menu_width(), geo_get_menu_height(),
geo_get_screen_width(), geo_get_screen_height(),
font_get());
menu.current_node->wid = ui->w[ui->cur].win;
}
static void checkout_parentmenu(char *tag)
{
checkout_tag(tag);
/* If we've used ^root(tag), we're alreday at top window */
if (!ui->cur)
return;
geo_win_del();
ui_win_del();
}
static void checkout_rootmenu(char *tag)
{
checkout_tag(tag);
geo_set_cur(0);
}
/* This checks out the original root menu, regardless of any ^root() action */
static void checkout_rootnode(void)
{
BUG_ON(!menu.current_node);
if (!menu.current_node->parent)
return;
while (menu.current_node->parent)
menu.current_node = menu.current_node->parent;
checkout_rootmenu(menu.current_node->item->tag);
}
static void resize(void)
{
XMoveResizeWindow(ui->dpy, ui->w[ui->cur].win, geo_get_menu_x0(),
geo_get_menu_y0(), geo_get_menu_width(),
geo_get_menu_height());
}
static void update(int resize_required)
{
update_filtered_list();
init_menuitem_coordinates();
draw_menu();
if (!resize_required)
return;
resize();
ui_map_window(geo_get_menu_width(), geo_get_menu_height());
}
static void launch_menu_at_pointer(void)
{
Window dw;
int di;
unsigned int du;
struct point pos;
struct area wa;
geo_update_monitor_coords();
XQueryPointer(ui->dpy, DefaultRootWindow(ui->dpy), &dw, &dw, &di, &di,
&pos.x, &pos.y, &du);
if (pos.x < config.edge_snap_x)
pos.x = 0;
/* We use config.menu_{v,h}align to tell us where the panel is */
if (config.menu_valign == TOP)
wa.y = geo_get_screen_y0() + config.menu_margin_y;
else
wa.y = geo_get_screen_y0();
wa.h = geo_get_screen_height() - config.menu_margin_y;
if (config.menu_halign == LEFT)
wa.x = geo_get_screen_x0() + config.menu_margin_x;
else
wa.x = geo_get_screen_x0();
wa.w = geo_get_screen_width() - config.menu_margin_x;
if (pos.x < wa.w + wa.x - geo_get_menu_width()) {
geo_set_menu_halign(LEFT);
if (config.menu_halign == LEFT && pos.x < config.menu_margin_x)
pos.x = config.menu_margin_x;
geo_set_menu_margin_x(pos.x);
} else {
geo_set_menu_halign(RIGHT);
if (config.menu_halign == RIGHT && pos.x > wa.w)
pos.x = wa.w;
geo_set_menu_margin_x(geo_get_screen_width() - pos.x);
}
if (pos.y < wa.h + wa.y - geo_get_menu_height()) {
geo_set_menu_valign(TOP);
if (config.menu_valign == TOP && pos.y < config.menu_margin_y)
pos.y = config.menu_margin_y;
geo_set_menu_margin_y(pos.y);
} else if (geo_get_menu_height() < pos.y) {
geo_set_menu_valign(BOTTOM);
if (config.menu_valign == BOTTOM && pos.y > wa.h)
pos.y = wa.h;
geo_set_menu_margin_y(geo_get_screen_height() - pos.y);
} else {
geo_set_menu_valign(BOTTOM);
if (config.menu_valign == BOTTOM)
geo_set_menu_margin_y(config.menu_margin_y);
else
geo_set_menu_margin_y(0);
}
set_submenu_width();
}
static void if_unity_run_hack(void)
{
static int first_run = 1;
static int isunity;
char *s;
if (first_run) {
s = getenv("JGMENU_UNITY");
if (s && s[0] == '1')
isunity = 1;
first_run = 0;
}
if (isunity)
spawn("jgmenu_run unity-hack", NULL);
}
static void awake_menu(void)
{
menu_is_hidden = 0;
if_unity_run_hack();
if (watch_files_have_changed())
restart();
if (config.position_mode == POSITION_MODE_PTR) {
launch_menu_at_pointer();
resize();
}
ipc_read_socket();
if (config.position_mode == POSITION_MODE_IPC) {
ipc_align_based_on_env_vars();
update(1);
}
XMapWindow(ui->dpy, ui->w[ui->cur].win);
ui_map_window(geo_get_menu_width(), geo_get_menu_height());
XRaiseWindow(ui->dpy, ui->w[ui->cur].win);
grabkeyboard();
grabpointer();
}
static int node_exists(const char *name)
{
struct node *n;
BUG_ON(!name);
list_for_each_entry(n, &menu.nodes, node)
if (!strcmp(name, n->item->tag))
return 1;
return 0;
}
static void create_node(const char *name, struct node *parent)
{
struct node *n;
n = xmalloc(sizeof(struct node));
BUG_ON(!name);
n->item = get_item_from_tag(name);
n->last_sel = NULL;
n->last_first = NULL;
n->expanded = NULL;
n->parent = parent;
n->wid = 0;
list_add_tail(&n->node, &menu.nodes);
}
/* Create nodal tree from tagged items */
static struct node *node_add_new(struct item *this, struct node *parent)
{
struct item *child, *p;
struct node *current_node;
BUG_ON(!this);
BUG_ON(!this->tag);
if (tag_count(this->tag) > 1)
die("duplicate tag (%s)", this->tag);
create_node(this->tag, parent);
/* move to next item, as this points to a ^tag() item */
p = container_of((this)->master.next, struct item, master);
/* p now points to first menu-item under tag "this->tag" */
current_node = list_last_entry(&menu.nodes, struct node, node);
/* walk the items under current node and put into tree structure */
list_for_each_entry_from(p, &menu.master, master) {
if (!strncmp("^checkout(", p->cmd, 10)) {
child = get_item_from_tag(parse_caret_action(p->cmd, "^checkout("));
if (!child)
continue;
if (child->tag && node_exists(child->tag))
continue;
node_add_new(child, current_node);
} else if (!strncmp("^tag(", p->cmd, 5)) {
break;
}
}
return current_node;
}
static void destroy_node_tree(void)
{
struct node *n, *tmp_n;
list_for_each_entry_safe(n, tmp_n, &menu.nodes, node) {
list_del(&n->node);
xfree(n);
}
}
static void build_tree(void)
{
struct item *item;
struct node *n, *root_node;
BUG_ON(list_empty(&menu.master));
item = list_first_entry_or_null(&menu.master, struct item, master);
BUG_ON(!item->tag);
BUG_ON(list_is_singular(&menu.master));
root_node = node_add_new(get_item_from_tag(item->tag), NULL);
/*
* Add any remaining ^tag()s - i.e. those without a corresponding
* ^checkout(). These are added to the top-level node.
*/
list_for_each_entry(item, &menu.master, master) {
BUG_ON(!item);
if (!item->tag)
continue;
list_for_each_entry(n, &menu.nodes, node)
if (n->item == item)
goto already_exists;
create_node(item->tag, root_node);
already_exists:
;
}
}
void remove_checkouts_without_matching_tags(void)
{
struct item *i, *tmp;
list_for_each_entry_safe(i, tmp, &menu.master, master) {
if (!strncmp(i->cmd, "^checkout(", 10) &&
!tag_exists(i->cmd + 10)) {
info("remove (%s) as it has no matching tag", i->cmd);
xfree(i->buf);
list_del(&i->master);
xfree(i);
}
}
}
/*
* The first item of a rootmenu or pipe must be a ^tag() item. If the user has not
* provided it, we tag care of it here.
*/
#define UTAG_BIG_NR (99999)
#define UTAG_BUFSIZ (18)
static void get_unique_tag_item(char *utag)
{
int i;
for (i = 0; i < UTAG_BIG_NR; i++) {
snprintf(utag, UTAG_BUFSIZ, "%d", i);
if (!node_exists(utag))
break;
}
snprintf(utag, UTAG_BUFSIZ, "%d,^tag(%d", i, i);
}
static void insert_tag_item(void)
{
struct item *item = NULL;
struct argv_buf argv_buf;
char utag[UTAG_BUFSIZ];
get_unique_tag_item(utag);
argv_set_delim(&argv_buf, ',');
argv_init(&argv_buf);
argv_strdup(&argv_buf, utag);
argv_parse(&argv_buf);
item = xmalloc(sizeof(struct item));
item->buf = argv_buf.buf;
item->name = argv_buf.argv[0];
item->cmd = argv_buf.argv[1];
item->iconname = NULL;
item->working_dir = NULL;
item->metadata = NULL;
item->icon = NULL;
item->tag = item->cmd + 5;
item->selectable = 1;
item->area.h = config.item_height;
list_add_tail(&item->master, &menu.master);
}
static void resolve_newline(char *s)
{
char *p;
if (!s)
return;
p = strstr(s, "\\n");
if (!p)
return;
*p = ' ';
*(p + 1) = '\n';
}
/**
* read_csv_file - read lines from FILE to "master" list
* @fp: file to be read
* @ispipemenu: ensure first item is ^tag(...) for pipemenus
*
* Return number of lines read
*/
static int read_csv_file(FILE *fp, bool ispipemenu)
{
char buf[BUFSIZ], *p;
size_t i;
struct item *item = NULL;
struct argv_buf argv_buf;
static bool first_item = true;
if (!fp)
die("no csv-file");
if (ispipemenu)
first_item = true;
argv_set_delim(&argv_buf, ',');
for (i = 0; fgets(buf, sizeof(buf), fp); i++) {
buf[BUFSIZ - 1] = '\0';
if (strlen(buf) == BUFSIZ - 1)
die("item %d is too long", i);
p = strrchr(buf, '\n');
if (p)
*p = '\0';
else
die("item %d was not correctly terminated with a '\\n'", i);
if (!utf8_validate(buf, p - &buf[0])) {
warn("line not utf-8 compatible: '%s'", buf);
i--;
continue;
}
if ((buf[0] == '#') ||
(buf[0] == '\n') ||
(buf[0] == '\0')) {
i--;
continue;
} else if (buf[0] == '@') {
widgets_add(buf);
continue;
} else if (buf[0] == '.' && buf[1] == ' ') {
FILE *include_file;
struct sbuf filename;
sbuf_init(&filename);
sbuf_cpy(&filename, buf + 1);
sbuf_trim(&filename);
sbuf_expand_tilde(&filename);
include_file = fopen(filename.buf, "r");
if (include_file) {
read_csv_file(include_file, false);
fclose(include_file);
}
xfree(filename.buf);
continue;
}
argv_init(&argv_buf);
argv_strdup(&argv_buf, buf);
argv_parse(&argv_buf);
item = xmalloc(sizeof(struct item));
item->buf = argv_buf.buf;
item->name = argv_buf.argv[0];
resolve_newline(item->name);
item->cmd = argv_buf.argv[1];
item->iconname = argv_buf.argv[2];
item->working_dir = argv_buf.argv[3];
item->metadata = argv_buf.argv[4];
remove_caret_markup_closing_bracket(item->name);
remove_caret_markup_closing_bracket(item->cmd);
if (!item->cmd)
item->cmd = item->name;
if (first_item) {
if (item->cmd && strncmp(item->cmd, "^tag(", 5))
insert_tag_item();
first_item = false;
}
item->icon = NULL;
if (!strncmp("^tag(", item->cmd, 5))
item->tag = parse_caret_action(item->cmd, "^tag(");
else
item->tag = NULL;
item->selectable = 1;
item->area.h = config.item_height;
if (!strncmp(item->name, "^sep(", 5)) {
item->selectable = 0;
if (item->name[5] == '\0')
item->area.h = config.sep_height;
}
list_add_tail(&item->master, &menu.master);
}
return i;
}
static void rm_back_items(void)
{
struct item *i, *tmp;
list_for_each_entry_safe(i, tmp, &menu.master, master) {
if (!strncmp(i->cmd, "^back(", 6)) {
xfree(i->buf);
list_del(&i->master);
xfree(i);
}
}
}
static int is_ancestor_to_current_node(struct node *node)
{
struct node *n;
if (!node)
return 0;
n = menu.current_node;
while (n->parent) {
n = n->parent;
if (n == node)
return 1;
}
return 0;
}
static void recalc_expanded_nodes(void)
{
struct node *n;
menu.current_node->expanded = NULL;
list_for_each_entry(n, &menu.nodes, node)
if (!is_ancestor_to_current_node(n))
n->expanded = NULL;
}
/* Delete windows and pipemenus beyond current node */
static void del_beyond_current(void)
{
ui_win_del_beyond(ui->cur);
pipemenu_del_beyond(menu.current_node);
recalc_expanded_nodes();
}
static void del_beyond_root(void)
{
ui_win_del_beyond(0);
geo_set_cur(0);
checkout_rootnode(); /* original root node */
pipemenu_del_all();
}
static int check_pipe_tags_unique(struct item *from)
{
struct item *p;
p = from;
list_for_each_entry_from(p, &menu.master, master) {
BUG_ON(!p);
if (!p->tag)
continue;
if (node_exists(p->tag)) {
warn("tag (%s) already exists", p->tag);
return -1;
}
}
return 0;
}
static void destroy_master_list_from(struct item *from)
{
struct item *i, *i_tmp;
i = from;
list_for_each_entry_safe_from(i, i_tmp, &menu.master, master) {
xfree(i->buf);
list_del(&i->master);
xfree(i);
}
}
static void pipemenu_add(const char *s)
{
FILE *fp = NULL;
struct item *pipe_head;
struct node *parent_node;
int nr_lines;
BUG_ON(!s);
fp = popen(s, "r");
if (!fp) {
warn("could not open pipe '%s'", s);
return;
}
pipe_head = list_last_entry(&menu.master, struct item, master);
nr_lines = read_csv_file(fp, true);
if (fp && fp != stdin)
pclose(fp);
if (!nr_lines) {
warn("empty pipemenu");
return;
}
pipe_head = container_of(pipe_head->master.next, struct item, master);
/* pipe_head now points to first item of pipe */
if (check_pipe_tags_unique(pipe_head) < 0) {
destroy_master_list_from(pipe_head);
info("pipe menu removed");
return;
}
if (config.hide_back_items)
rm_back_items();
parent_node = menu.current_node;
remove_checkouts_without_matching_tags();
node_add_new(pipe_head, parent_node);
checkout_submenu(pipe_head->tag);
pm_push(menu.current_node, parent_node);
}
/**
* pipemenu_del_from - remove nodes and menu items associated with pipemenu
* @node: delete pipemenu from this point onwards
* Note: 'node' needs to be within a pipemenu
*/
static void pipemenu_del_from(struct node *node)
{
struct node *n_tmp;
pm_pop();
destroy_master_list_from(node->item);
list_for_each_entry_safe_from(node, n_tmp, &menu.nodes, node) {
list_del(&node->node);
xfree(node);
}
}
static void pipemenu_del_beyond(struct node *keep_me)
{
struct node *n, *n_tmp;
list_for_each_entry_safe_reverse(n, n_tmp, &menu.nodes, node) {
if (n == keep_me)
break;
/* we can't remove nodes within a pipemenu */
if (pm_is_pipe_node(n))
pipemenu_del_from(n);
}
}
static void pipemenu_del_all(void)
{
struct node *n;
n = (struct node *)pm_first_pipemenu_node();
if (!n)
return;
pipemenu_del_from(n);
pm_cleanup();
}
static void checkout_parent(void)
{
struct node *parent;
if (!menu.current_node->parent)
return;
parent = menu.current_node->parent;
if (pm_is_pipe_node(menu.current_node))
pipemenu_del_from(menu.current_node);
/* reverting to parent following ^root() */
if (!parent->wid) {
parent->wid = menu.current_node->wid;
menu.current_node->wid = 0;
}
checkout_parentmenu(parent->item->tag);
if (config.menu_height_mode == CONFIG_DYNAMIC)
set_submenu_height();
}
static void clear_self_pipe(void)
{
fd_set readfds;
if (!FD_ISSET(pipe_fds[0], &readfds))
return;
for (;;) {
char ch;
if (read(pipe_fds[0], &ch, 1) == -1) {
if (errno == EAGAIN)
break;
warn("error reading pipe");
}
}
}
static void hide_menu(void)
{
tmr_mouseover_stop();
del_beyond_root();
XUnmapWindow(ui->dpy, ui->w[ui->cur].win);
filter_reset();
XUngrabKeyboard(ui->dpy, CurrentTime);
XUngrabPointer(ui->dpy, CurrentTime);
menu.current_node->expanded = NULL;
menu.sel = NULL;
update(1);
clear_self_pipe();
menu_is_hidden = 1;
}
static void hide_or_exit(void)
{
if (config.stay_alive)
hide_menu();
else
exit(0);
}
static void action_cmd(char *cmd, const char *working_dir)
{
char *p = NULL;
if (!cmd)
return;
if (!config.spawn && strncmp("^checkout(", cmd, 10) &&
strncmp("^sub(", cmd, 5) && strncmp("^back(", cmd, 6) &&
strncmp("^pipe(", cmd, 6)) {
printf("%s\n", cmd);
exit(0);
}
if (!strncmp(cmd, "^checkout(", 10)) {
p = parse_caret_action(cmd, "^checkout(");
if (!p)
return;
if (!tag_exists(p))
return;
menu.current_node->last_sel = menu.sel;
menu.current_node->last_first = menu.first;
checkout_submenu(p);
update(1);
} else if (!strncmp(cmd, "^sub(", 5)) {
p = parse_caret_action(cmd, "^sub(");
if (!p)
return;
spawn(p, working_dir);
hide_or_exit();
} else if (!strncmp(cmd, "^back(", 6)) {
checkout_parent();
update(1);
} else if (!strncmp(cmd, "^term(", 6)) {
struct sbuf s;
p = parse_caret_action(cmd, "^term(");
if (!p)
return;
sbuf_init(&s);
term_build_terminal_cmd(&s, strstrip(p), config.terminal_exec,
config.terminal_args);
spawn(s.buf, working_dir);
free(s.buf);
hide_or_exit();
} else if (!strncmp(cmd, "^pipe(", 6)) {
p = parse_caret_action(cmd, "^pipe(");
if (!p)
return;
menu.current_node->last_sel = menu.sel;
menu.current_node->last_first = menu.first;
pipemenu_add(p);
update(1);
} else if (!strncmp(cmd, "^root(", 6)) {
/* Two nodes with the same wid breaks get_node_from_wid() */
if (!tag_exists(cmd + 6))
return;
menu.current_node->last_sel = menu.sel;
menu.current_node->last_first = menu.first;
menu.current_node->wid = 0;
del_beyond_root();
filter_reset();
checkout_tag(cmd + 6);
menu.current_node->wid = ui->w[ui->cur].win;
if (config.menu_height_mode == CONFIG_DYNAMIC) {
set_submenu_height();
update(1);
} else {
update(0);
}
} else if (!strncmp(cmd, "^filter(", 8)) {
p = parse_caret_action(cmd, "^filter(");
if (!p)
return;
filter_reset();
filter_set_clear_on_keyboard_input(1);
filter_addstr(p, strlen(p));
update(1);
} else {
spawn(cmd, working_dir);
hide_or_exit();
}
}
static struct point mousexy(void)
{
Window dw;
int di;
unsigned int du;
struct point coords;
XQueryPointer(ui->dpy, ui->w[ui->cur].win, &dw, &dw, &di, &di, &coords.x,
&coords.y, &du);
return coords;
}
static void key_event(XKeyEvent *ev)
{
char buf[32];
int len;
KeySym ksym = NoSymbol;
Status status;
int isoutside;
len = Xutf8LookupString(ui->w[ui->cur].xic, ev, buf, sizeof(buf), &ksym, &status);
if (status == XBufferOverflow)
return;
if (ui_has_child_window_open(menu.current_node->wid))
del_beyond_current();
/* menu.sel == NULL could be caused by pointer movement */
if (!menu.sel)
menu.sel = menu.current_node->last_sel;
switch (ksym) {
case XK_Tab:
widgets_toggle_kb_grabbed();
break;
case XK_End:
if (filter_head() == &empty_item)
break;
menu.last = filter_tail();
menu.first = fill_from_bottom(menu.last);
menu.sel = last_selectable();
init_menuitem_coordinates();
menu.current_node->last_sel = menu.sel;
draw_menu();
break;
case XK_Super_L:
case XK_Super_R:
super_key_pressed = 1;
break;
case XK_Escape:
if (filter_needle_length()) {
filter_reset();
update(0);
} else {
hide_or_exit();
}
break;
case XK_Home:
if (filter_head() == &empty_item)
break;
menu.first = filter_head();
menu.last = fill_from_top(menu.first);
menu.sel = first_selectable();
init_menuitem_coordinates();
menu.current_node->last_sel = menu.sel;
draw_menu();
break;
case XK_Up:
if (widgets_get_kb_grabbed()) {
widgets_select("XK_Up");
draw_menu();
action_cmd(widgets_get_selection_action(), NULL);
break;
}
if (filter_head() == &empty_item)
break;
if (menu.sel == first_selectable()) {
/* bounce to bottom */
menu.last = filter_tail();
menu.first = fill_from_bottom(menu.last);
menu.sel = last_selectable();
} else {
menu.sel = prev_selectable(menu.sel, &isoutside);
if (isoutside) {
menu.first = menu.sel;
menu.last = fill_from_top(menu.first);
}
}
init_menuitem_coordinates();
menu.current_node->last_sel = menu.sel;
draw_menu();
break;
case XK_Next: /* PageDown */
if (filter_head() == &empty_item)
break;
menu.first = menu.last;
if (menu.first != filter_tail())
step_fwd(&menu.first, 1);
menu.last = fill_from_top(menu.first);
if (menu.last == filter_tail())
menu.first = fill_from_bottom(menu.last);
init_menuitem_coordinates();
menu.sel = menu.last;
if (!menu.last->selectable)
menu.sel = prev_selectable(menu.last, &isoutside);
menu.current_node->last_sel = menu.sel;
draw_menu();
break;
case XK_Prior: /* PageUp */
if (filter_head() == &empty_item)
break;
menu.last = menu.first;
if (menu.last != filter_head())
step_back(&menu.last, 1);
menu.first = fill_from_bottom(menu.last);
if (menu.first == filter_head())
menu.last = fill_from_top(menu.first);
init_menuitem_coordinates();
menu.sel = menu.first;
if (!menu.sel->selectable)
menu.sel = next_selectable(menu.first, &isoutside);
menu.current_node->last_sel = menu.sel;
draw_menu();
break;
case XK_Return:
case XK_KP_Enter:
if (menu.sel->selectable)
action_cmd(menu.sel->cmd, menu.sel->working_dir);
break;
case XK_Down:
if (widgets_get_kb_grabbed()) {
widgets_select("XK_Down");
draw_menu();
action_cmd(widgets_get_selection_action(), NULL);
break;
}
if (filter_head() == &empty_item)
break;
if (menu.sel == last_selectable()) {
menu.first = filter_head();
menu.last = fill_from_top(menu.first);
menu.sel = first_selectable();
} else {
menu.sel = next_selectable(menu.sel, &isoutside);
if (isoutside) {
menu.last = menu.sel;
menu.first = fill_from_bottom(menu.last);
}
}
init_menuitem_coordinates();
menu.current_node->last_sel = menu.sel;
draw_menu();
break;
case XK_Left:
if (!menu.current_node->parent)
break;
checkout_parent();
update(1);
break;
case XK_Right:
if (!strncmp(menu.sel->cmd, "^checkout(", 10) ||
!strncmp(menu.sel->cmd, "^root(", 6) ||
!strncmp(menu.sel->cmd, "^pipe(", 6))
action_cmd(menu.sel->cmd, NULL);
break;
case XK_F5:
restart();
break;
case XK_F7:
break;
case XK_F8:
print_nodes();
break;
case XK_F9:
/* Useful for stopping Makefile during tests/ or examples/ */
exit(1);
break;
case XK_F10:
/* force exit even if in 'stay_alive' mode */
exit(0);
break;
case XK_BackSpace:
if (filter_needle_length()) {
filter_backspace();
update(0);
} else {
checkout_parent();
update(1);
}
break;
default:
if (filter_get_clear_on_keyboard_input())
filter_reset();
filter_addstr(buf, len);
update(0);
break;
}
}
static int is_outside_menu_windows(XMotionEvent **e)
{
struct node *n;
if (!(*e)->subwindow)
return 1;
list_for_each_entry(n, &menu.nodes, node)
if ((*e)->subwindow == n->wid)
return 0;
return 1;
}
/* Pointer vertical offset (not sure why this is needed) */
#define MOUSE_FUDGE 3
static void mouse_release(XEvent *e)
{
XButtonReleasedEvent *ev;
struct point mouse_coords;
mouse_coords = mousexy();
mouse_coords.y -= MOUSE_FUDGE;
ev = &e->xbutton;
/* scroll up */
if (ev->button == Button4 && menu.first != filter_head()) {
if (!menu.sel)
menu.sel = menu.current_node->last_sel;
if (ui_has_child_window_open(menu.current_node->wid))
del_beyond_current();
step_back(&menu.first, 1);
menu.last = fill_from_top(menu.first);
step_back(&menu.sel, 1);
init_menuitem_coordinates();
menu.current_node->last_sel = menu.sel;
draw_menu();
return;
}
/* scroll down */
if (ev->button == Button5 && menu.last != filter_tail()) {
if (!menu.sel)
menu.sel = menu.current_node->last_sel;
if (ui_has_child_window_open(menu.current_node->wid))
del_beyond_current();
step_fwd(&menu.last, 1);
menu.first = fill_from_bottom(menu.last);
step_fwd(&menu.sel, 1);
init_menuitem_coordinates();
menu.current_node->last_sel = menu.sel;
draw_menu();
return;
}
/* left-click */
if (ev->button == Button1) {
char *ret;
/* widgets */
widgets_set_pointer_position(mouse_coords.x, mouse_coords.y);
ret = widgets_get_selection_action();
if (ret && ret[0] != '\0') {
action_cmd(ret, NULL);
return;
}
/* normal menu items */
if (!menu.sel)
return;
if (!menu.sel->selectable)
return;
if (config.sub_hover_action &&
(!strncmp(menu.sel->cmd, "^checkout(", 10) ||
!strncmp(menu.sel->cmd, "^pipe(", 6)))
return;
if (ui_has_child_window_open(menu.current_node->wid))
del_beyond_current();
action_cmd(menu.sel->cmd, menu.sel->working_dir);
}
}
static int mouse_outside(XEvent *e)
{
XMotionEvent *xme;
XButtonReleasedEvent *ev;
struct point mouse_coords;
int outside_menu_windows = 0;
mouse_coords = mousexy();
mouse_coords.y -= MOUSE_FUDGE;
xme = (XMotionEvent *)e;
if (is_outside_menu_windows(&xme))
outside_menu_windows = 1;
ev = &e->xbutton;
/* left/right-click outside menu windows */
if ((ev->button == Button1 || ev->button == Button3) &&
outside_menu_windows)
return 1;
return 0;
}
static double timespec_to_sec(struct timespec *ts)
{
return (double)ts->tv_sec + (double)ts->tv_nsec / 1000000000.0;
}
/*
* This function is loaded in the background under a new pthread
* X11 is not thread-safe, so load_icons() must not call any X functions.
*/
static void *load_icons(void *arg)
{
struct timespec ts_start;
struct timespec ts_end;
double duration;
clock_gettime(CLOCK_MONOTONIC, &ts_start);
icon_load();
clock_gettime(CLOCK_MONOTONIC, &ts_end);
if (DEBUG_ICONS_LOADED_NOTIFICATION) {
duration = timespec_to_sec(&ts_end) - timespec_to_sec(&ts_start);
fprintf(stderr, "Icons loaded in %f seconds\n", duration);
}
if (write(pipe_fds[1], "x", 1) == -1)
die("error writing to icon_pipe");
return NULL;
}
static void destroy_master_list(void)
{
struct item *item, *tmp_item;
list_for_each_entry_safe(item, tmp_item, &menu.master, master) {
xfree(item->buf);
list_del(&item->master);
xfree(item);
}
}
static void init_pipe_flags(void)
{
int flags;
flags = fcntl(pipe_fds[0], F_GETFL);
if (flags == -1)
die("error getting pipe flags");
flags |= O_NONBLOCK;
if (fcntl(pipe_fds[0], F_SETFL, flags) == -1)
die("error setting pipe flags");
flags = fcntl(pipe_fds[1], F_GETFL);
if (flags == -1)
die("error getting pipe flags");
flags |= O_NONBLOCK;
if (fcntl(pipe_fds[1], F_SETFL, flags) == -1)
die("error setting pipe flags");
}
static void move_selection_with_mouse(struct point *mouse_coord)
{
struct item *item;
item = menu.first;
list_for_each_entry_from(item, &menu.filter, filter) {
if (!item->selectable)
continue;
if (ui_is_point_in_area(*mouse_coord, item->area)) {
if (menu.sel != item) {
menu.sel = item;
menu.current_node->last_sel = item;
draw_menu();
}
return;
}
if (item == menu.last)
break;
}
/* if we got this far, the mouse is not over a menu item */
if (!menu.current_node->expanded && menu.sel) {
menu.sel = NULL;
draw_menu();
}
if (widgets_mouseover())
draw_menu();
}
static void mouseover_handler(int sig)
{
int saved_errno;
saved_errno = errno;
if (write(pipe_fds[1], "t", 1) == -1)
die("error writing to pipe for onmouseover timer");
errno = saved_errno;
}
static void tmr_mouseover_init(void)
{
if (signal(SIGALRM, (void *)mouseover_handler) == SIG_ERR)
die("SIGALRM action");
}
static void tmr_mouseover_set(int msec)
{
struct itimerval it;
static int run_once;
if (msec >= 1000)
msec = 999;
if (!run_once) {
tmr_mouseover_init();
run_once = 1;
}
it.it_value.tv_sec = 0;
it.it_value.tv_usec = msec * 1000;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 0;
/* As timer_*() are not supported on OpenBSD, we use setitimer() */
if (setitimer(ITIMER_REAL, &it, NULL) == -1)
die("setitimer()");
}
static void tmr_mouseover_start(void)
{
tmr_mouseover_set(config.hover_delay);
}
static void tmr_mouseover_stop(void)
{
tmr_mouseover_set(0);
}
static struct node *get_node_from_wid(Window w)
{
struct node *n;
if (!w)
return NULL;
list_for_each_entry(n, &menu.nodes, node)
if (w == n->wid)
return n;
return NULL;
}
static void close_sub_window(void)
{
if (!ui_has_child_window_open(menu.current_node->wid))
return;
sw_close_pending = 1;
tmr_mouseover_start();
}
static void hover(void)
{
/*
* Mouse is over an already "expanded" item (i.e. one that caused a
* sub window to open
*/
if (menu.sel == menu.current_node->expanded) {
tmr_mouseover_stop();
return;
}
if (menu.sel && (!strncmp(menu.sel->cmd, "^checkout(", 10) ||
!strncmp(menu.sel->cmd, "^pipe(", 6))) {
tmr_mouseover_start();
sw_close_pending = 0;
return;
}
/* non-submenu item */
if (!sw_close_pending) {
tmr_mouseover_stop();
close_sub_window();
}
if (widgets_mouseover()) {
tmr_mouseover_stop();
close_sub_window();
return;
}
}
static void set_focus(Window w)
{
struct node *n;
n = get_node_from_wid(w);
menu.current_node->last_sel = menu.sel;
ui_win_activate(w);
geo_set_cur(ui->cur);
checkout_tag(n->item->tag);
menu.sel = n->last_sel;
menu.current_node = n;
}
static void adjust_selection_and_redraw(void)
{
if (menu.current_node->expanded) {
/*
* When moving the pointer diagonally from a parent to
* a child window, the correct menu.sel needs to be set
* and redrawn in the parent window.
*/
menu.sel = menu.current_node->expanded;
menu.current_node->last_sel = menu.sel;
} else {
/*
* Don't show selection in submenu if we are not
* actually over the window
*/
menu.sel = NULL;
}
draw_menu();
}
static void process_pointer_position(XEvent *ev, int force)
{
struct point pw;
static int oldy;
static int oldx;
XMotionEvent *e = (XMotionEvent *)ev;
/*
* We get the mouse coordinates using XQueryPointer() as
* ev.xbutton.{x,y} sometimes returns peculiar values.
* If we ever need the coordinates relative to the root window, use
* e->x_root and e->y_root
*/
pw = mousexy();
pw.y -= MOUSE_FUDGE;
if (!force && (pw.x == oldx) && (pw.y == oldy))
return;
widgets_set_pointer_position(pw.x, pw.y);
/* e->subwindow refers to the window under the pointer */
if (e->subwindow == ui->w[ui->cur].win) {
move_selection_with_mouse(&pw);
if (config.sub_hover_action)
hover();
} else if (is_outside_menu_windows(&e)) {
adjust_selection_and_redraw();
tmr_mouseover_stop();
} else {
/*
* We end up here whenever we move from one window to another.
*
* When a sub window has just opened, we end up here on
* the next event (once) and re-set the focus on the 'parent'
* window.
*/
adjust_selection_and_redraw();
set_focus(e->subwindow);
update(1);
}
oldx = pw.x;
oldy = pw.y;
}
static void signal_handler(int sig)
{
int saved_errno;
saved_errno = errno;
if (write(pipe_fds[1], "1", 1) == -1 && errno != EAGAIN)
die("write");
errno = saved_errno;
}
static void run(void)
{
XEvent ev;
struct item *item;
int ready, nfds, x11_fd;
fd_set readfds;
static int all_icons_have_been_requested;
struct sigaction sa;
/* for performance testing */
if (args_die_when_loaded() && !config.icon_size)
exit(0);
FD_ZERO(&readfds);
nfds = 0;
/* Set x11 fd */
x11_fd = ConnectionNumber(ui->dpy);
nfds = MAX(nfds, x11_fd + 1);
FD_SET(x11_fd, &readfds);
/* Create icon pipe */
if (pipe(pipe_fds) == -1)
die("error creating pipe");
FD_SET(pipe_fds[0], &readfds);
nfds = MAX(nfds, pipe_fds[0] + 1);
init_pipe_flags();
if (config.icon_size) {
/*
* Get icons in top level menu (or the one specified with
* --check-out=
*/
item = menu.subhead;
list_for_each_entry_from(item, &menu.master, master) {
if (item->iconname)
icon_set_name(item->iconname);
if (item == menu.subtail)
break;
}
pthread_create(&thread, NULL, load_icons, NULL);
}
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = signal_handler;
if (sigaction(SIGUSR1, &sa, NULL) == -1)
die("sigaction");
if (config.hide_on_startup)
hide_menu();
for (;;) {
FD_ZERO(&readfds);
FD_SET(x11_fd, &readfds);
FD_SET(pipe_fds[0], &readfds);
/*
* XPending() is non-blocking whereas select() is blocking.
*
* Some X events are stored in a queue in memory, so we cannot
* rely on reading ConnectionNumber() alone to catch all events.
*/
ready = 0;
if (!XPending(ui->dpy))
ready = select(nfds, &readfds, NULL, NULL, NULL);
if (ready == -1 && errno == EINTR)
continue;
if (ready == -1)
die("select()");
/*
* Check if there is something in the selfpipe. E.g. the icon
* thread has finished or we have caught a USR1 signal
*/
if (FD_ISSET(pipe_fds[0], &readfds) && ready) {
for (;;) {
char ch;
if (read(pipe_fds[0], &ch, 1) == -1) {
if (errno == EAGAIN)
break;
die("error reading pipe");
}
/* Caught SIGUSR1 */
if (ch == '1') {
awake_menu();
continue;
}
/* mouse over signal */
if (ch == 't') {
BUG_ON(!menu.sel);
del_beyond_current();
/* open new sub window */
if (!sw_close_pending && !menu_is_hidden) {
menu.current_node->expanded = menu.sel;
action_cmd(menu.sel->cmd,
menu.sel->working_dir);
}
sw_close_pending = 0;
process_pointer_position(&ev, 1);
continue;
}
/* 'x' means that icons have finished loading */
if (ch != 'x')
continue;
/* for performance testing */
if (args_die_when_loaded() && all_icons_have_been_requested)
exit(0);
if (DEBUG_ICONS_LOADED_NOTIFICATION &&
all_icons_have_been_requested)
fprintf(stderr, "All icons loaded\n");
if (DEBUG_ICONS_LOADED_NOTIFICATION &&
!all_icons_have_been_requested)
fprintf(stderr, "Root menu icons loaded\n");
pthread_join(thread, NULL);
list_for_each_entry(item, &menu.master, master)
if (!item->icon)
item->icon = icon_get_surface(item->iconname);
draw_menu();
if (all_icons_have_been_requested)
continue;
/* Get remaining icons */
list_for_each_entry(item, &menu.master, master)
if (item->iconname)
icon_set_name(item->iconname);
pthread_create(&thread, NULL, load_icons, NULL);
all_icons_have_been_requested = 1;
}
}
if (XPending(ui->dpy)) {
static int close_pending;
XNextEvent(ui->dpy, &ev);
/* UTF-8 support */
if (XFilterEvent(&ev, ui->w[ui->cur].win))
continue;
switch (ev.type) {
case MappingNotify:
XRefreshKeyboardMapping(&ev.xmapping);
break;
case ButtonRelease:
if (close_pending) {
close_pending = 0;
hide_or_exit();
break;
}
mouse_release(&ev);
break;
case ButtonPress:
/*
* tint2 buttons/execps take action on
* "ButtonRelease". We want to be able to use
* these to both open and close the menu.
* When passing mouse events through tint2 to
* the WM, we want menu to be able to repsond
* to "ButtonPress" without immediately dying
* on "ButtonRelease".
*/
if (mouse_outside(&ev))
close_pending = 1;
break;
case KeyRelease:
if (super_key_pressed) {
super_key_pressed = 0;
/* avoid passing super key to WM */
msleep(30);
hide_or_exit();
}
break;
case KeyPress:
key_event(&ev.xkey);
break;
case Expose:
if (ev.xexpose.count == 0)
ui_map_window(geo_get_menu_width(),
geo_get_menu_height());
break;
case VisibilityNotify:
if (ev.xvisibility.state != VisibilityUnobscured)
XRaiseWindow(ui->dpy, ui->w[ui->cur].win);
break;
case MotionNotify:
process_pointer_position(&ev, 0);
break;
}
}
}
}
static void init_geo_variables_from_config(void)
{
geo_set_menu_halign(config.menu_halign);
geo_set_menu_valign(config.menu_valign);
geo_set_menu_margin_x(config.menu_margin_x);
geo_set_menu_margin_y(config.menu_margin_y);
geo_set_menu_padding_top(config.menu_padding_top);
geo_set_menu_padding_right(config.menu_padding_right);
geo_set_menu_padding_bottom(config.menu_padding_bottom);
geo_set_menu_padding_left(config.menu_padding_left);
geo_set_menu_width(config.menu_width);
geo_set_sub_spacing(config.sub_spacing);
geo_set_sub_padding_top(config.sub_padding_top);
geo_set_sub_padding_right(config.sub_padding_right);
geo_set_sub_padding_bottom(config.sub_padding_bottom);
geo_set_sub_padding_left(config.sub_padding_left);
geo_set_item_margin_x(config.item_margin_x);
geo_set_item_margin_y(config.item_margin_y);
geo_set_item_height(config.item_height);
}
static void set_font(void)
{
font_set();
info("set font to '%s'", font_get());
}
static void set_theme(void)
{
struct sbuf theme;
if (!config.icon_size)
return;
sbuf_init(&theme);
theme_set(&theme);
icon_init();
icon_set_size(config.icon_size);
info("set icon theme to '%s'", theme.buf);
icon_set_theme(theme.buf);
}
static void quit(int signum)
{
fprintf(stderr, "\ninfo: caught SIGTERM or SIGINT\n");
exit(0);
}
static void set_simple_mode(void)
{
config.stay_alive = 0;
config.tint2_look = 0;
}
static void read_tint2rc(void)
{
char *t;
struct sbuf bl;
sbuf_init(&bl);
t = getenv("TINT2_CONFIG");
if (t) {
t2conf_parse(t, geo_get_screen_width(),
geo_get_screen_height());
goto out;
}
bl_tint2file(&bl);
if (bl.len) {
info("using BunsenLabs tint2 session file");
t2conf_parse(bl.buf, geo_get_screen_width(),
geo_get_screen_height());
} else {
t2conf_parse(NULL, geo_get_screen_width(),
geo_get_screen_height());
}
out:
free(bl.buf);
}
static void init_locale(void)
{
if (!setlocale(LC_ALL, ""))
warn("setlocale(): locale not supported by C library; using 'C' locale");
if (!XSupportsLocale())
warn("XSupportsLocale(): error setting locale");
if (!XSetLocaleModifiers("@im=none"))
warn("XSetLocaleModifiers(): error setting locale");
}
static void init_sigactions(void)
{
struct sigaction term_action, int_action;
memset(&term_action, 0, sizeof(struct sigaction));
memset(&int_action, 0, sizeof(struct sigaction));
term_action.sa_handler = quit;
int_action.sa_handler = quit;
sigaction(SIGTERM, &term_action, NULL);
sigaction(SIGINT, &int_action, NULL);
}
static void cleanup(void)
{
ui_cleanup();
config_cleanup();
filter_cleanup();
font_cleanup();
if (config.icon_size)
icon_cleanup();
widgets_cleanup();
watch_cleanup();
t2conf_atexit();
delete_empty_item();
destroy_node_tree();
destroy_master_list();
}
static void keep_menu_height_between_min_and_max(void)
{
if (config.menu_height_min &&
geo_get_menu_height() < config.menu_height_min)
geo_set_menu_height(config.menu_height_min);
if (config.menu_height_max &&
geo_get_menu_height() > config.menu_height_max)
geo_set_menu_height(config.menu_height_max);
}
int main(int argc, char *argv[])
{
int i;
char *arg_config_file = NULL;
int arg_vsimple = 0;
FILE *fp = NULL;
args_exec_commands(argc, argv);
init_locale();
restart_init(argc, argv);
init_sigactions();
config_set_defaults();
menu.current_node = NULL;
INIT_LIST_HEAD(&menu.master);
INIT_LIST_HEAD(&menu.filter);
INIT_LIST_HEAD(&menu.nodes);
for (i = 1; i < argc; i++) {
if (!strncmp(argv[i], "--config-file=", 14))
arg_config_file = argv[i] + 14;
else if (!strncmp(argv[i], "--vsimple", 9))
arg_vsimple = 1;
else if (!strncmp(argv[i], "--version", 9))
version();
else if (!strncmp(argv[i], "--help", 6) ||
!strncmp(argv[i], "-h", 2))
usage();
}
if (!arg_vsimple)
config_read_jgmenurc(arg_config_file);
if (!config.verbosity)
mute_info();
args_parse(argc, argv);
if (args_simple() || arg_vsimple)
set_simple_mode();
if (arg_vsimple)
config.icon_size = 0;
/* check lockfile after --help and --version */
if (config.stay_alive)
lockfile_init();
if_unity_run_hack();
watch_init();
ui_init();
geo_init();
filter_init();
if (config.tint2_look)
read_tint2rc();
/*
* If _NET_WORKAREA is supported by the Window Manager, set
* config.menu_margin_{x|y} and config.{v|h}align if possible
*/
if (config.respect_workarea) {
workarea_set_margin();
workarea_set_panel_pos();
}
/* Parse jgmenurc again to overrule tint2rc and workarea values */
if (!arg_vsimple)
config_read_jgmenurc(arg_config_file);
args_parse(argc, argv);
config_post_process();
/* config variables will not be changed after this point */
set_font();
set_theme();
init_geo_variables_from_config();
if (config.position_mode == POSITION_MODE_IPC) {
ipc_init_socket();
ipc_align_based_on_env_vars();
}
if (args_csv_file())
fp = fopen(args_csv_file(), "r");
else if (args_csv_cmd())
fp = popen(args_csv_cmd(), "r");
else if (config.csv_cmd && config.csv_cmd[0] != '\0' &&
!args_simple() && !arg_vsimple)
fp = popen(config.csv_cmd, "r");
if (!fp)
fp = stdin;
read_csv_file(fp, false);
if (fp && fp != stdin)
fclose(fp);
if (config.hide_back_items)
rm_back_items();
/*
* read_csv_file() ensures that the first item is a ^tag() one.
* In addition to this, we need at least one menu item to enable us
* to generate a menu.
*/
if (list_empty(&menu.master) || list_is_singular(&menu.master))
die("file did not contain any menu items");
remove_checkouts_without_matching_tags();
build_tree();
if (args_checkout())
checkout_rootmenu(args_checkout());
else
checkout_rootmenu(tag_of_first_item());
set_submenu_height();
set_submenu_width();
keep_menu_height_between_min_and_max();
grabkeyboard();
grabpointer();
if (config.position_mode == POSITION_MODE_PTR)
launch_menu_at_pointer();
if (config.menu_height_mode == CONFIG_DYNAMIC)
/*
* Make canvases the full size of the monitor to allow for
* future changes in menu size on ^root().
*/
ui_win_init(geo_get_menu_x0(), geo_get_menu_y0(),
geo_get_menu_width(), geo_get_menu_height(),
geo_get_screen_width(), geo_get_screen_height(),
font_get());
else
/* Make canvas just big enough */
ui_win_init(geo_get_menu_x0(), geo_get_menu_y0(),
geo_get_menu_width(), geo_get_menu_height(),
geo_get_menu_width(), geo_get_menu_height(),
font_get());
init_empty_item();
update_filtered_list();
init_menuitem_coordinates();
if (config.hide_on_startup)
info("menu started in 'hidden' mode; show by `jgmenu_run`");
else
XMapRaised(ui->dpy, ui->w[ui->cur].win);
draw_menu();
atexit(cleanup);
menu.current_node->wid = ui->w[ui->cur].win;
run();
return 0;
}
You can’t perform that action at this time.