#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include "pistachio.h"
#define WINDOW_TITLE "pistachio"
#define ABOVE_CURSOR_RATIO 9/10
#define BELOW_CURSOR_RATIO 2/10
#define VERT_GAP_RATIO 4/3
#define BORDER_PX 10
#define MENU_SIZE 1024
typedef struct {
Window window;
GC gc;
Visual *visual;
Colormap colormap;
int depth;
int window_w;
int window_h;
} Draw_Info;
typedef struct {
int *menu;
int n_items;
int selected;
int top;
int visible;
} Menu_View;
XImage search_chars[N_CHARS] = {0};
XImage results_chars[N_CHARS] = {0};
XImage sel_chars[N_CHARS] = {0};
XImage error_chars[N_CHARS] = {0};
Display *display = NULL;
void make_32bpp_ximage(Display *dpy, Visual *visual, u8 *data, int w, int h, XImage *image) {
image->width = w;
image->height = h;
image->format = ZPixmap;
image->byte_order = ImageByteOrder(dpy);
image->bitmap_unit = BitmapUnit(dpy);
image->bitmap_bit_order = BitmapBitOrder(dpy);
image->bitmap_pad = BitmapPad(dpy);
image->red_mask = visual->red_mask;
image->green_mask = visual->green_mask;
image->blue_mask = visual->blue_mask;
image->xoffset = 0;
image->depth = 32;
image->data = (char*)data;
image->bytes_per_line = w * 4;
image->bits_per_pixel = 32;
image->obdata = NULL;
XInitImage(image);
}
void create_window(Settings *config, Screen_Info *screen_info, Draw_Info *draw_ctx) {
XVisualInfo info;
XMatchVisualInfo(display, screen_info->idx, 32, TrueColor, &info);
XSetWindowAttributes attr;
attr.colormap = XCreateColormap(display, DefaultRootWindow(display), info.visual, AllocNone);
attr.border_pixel = 0;
attr.background_pixel = config->back_color;
attr.win_gravity = CenterGravity;
int w = (float)screen_info->w * config->window_w;
int h = (float)screen_info->h * config->window_h;
draw_ctx->visual = info.visual;
draw_ctx->colormap = attr.colormap;
draw_ctx->depth = info.depth;
draw_ctx->window_w = w;
draw_ctx->window_h = h;
draw_ctx->window = XCreateWindow(
display, RootWindow(display, screen_info->idx),
0, 0, w, h,
0, info.depth, InputOutput, info.visual,
CWColormap | CWBorderPixel | CWBackPixel | CWWinGravity,
&attr
);
}
void remove_window_border(Display *dpy, Window window) {
struct {
unsigned long flags;
unsigned long functions;
unsigned long decorations;
long input_mode;
unsigned long status;
} hints = {
.flags = (1L << 1), // decorations hint flag
.decorations = 0
};
Atom hint_msg = XInternAtom(display, "_MOTIF_WM_HINTS", false);
XChangeProperty(display, window, hint_msg, hint_msg, 32, PropModeReplace, (u8*)&hints, 5);
}
// This function lets us avoid having the window in the taskbar.
// Note: this function must be called AFTER XMapRaised()
void skip_taskbar(Display *dpy, Window window, Window root) {
XClientMessageEvent xclient = {
.type = ClientMessage,
.window = window,
.message_type = XInternAtom(display, "_NET_WM_STATE", false),
.format = 32, // 32 bits per data member
.data.l[0] = 1, // Add/set property
.data.l[1] = XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", false),
.data.l[2] = 0, // Second property to set = None
.data.l[3] = 1, // 1 for client apps, 2 for pager apps
.data.l[4] = 0 // no more values
};
XSendEvent(
dpy, root, false,
SubstructureRedirectMask | SubstructureNotifyMask,
(XEvent*)&xclient
);
}
void draw_string(char *text, int length, int offset, int *cursor, int x, int y, Draw_Info *draw_ctx, Glyph *glyphs) {
if (length < 1)
length = strlen(text);
int caret_y1 = y - FONT_HEIGHT(glyphs[0]) * ABOVE_CURSOR_RATIO;
int caret_y2 = y + FONT_HEIGHT(glyphs[0]) * BELOW_CURSOR_RATIO;
if (text) {
for (int i = offset; i < length && x < draw_ctx->window_w - BORDER_PX; i++) {
int idx = glyph_indexof(text[i]);
if (idx < 0)
continue;
if (glyphs[idx].data)
XPutImage(
display, draw_ctx->window, draw_ctx->gc, (XImage*)&glyphs[idx].ximage,
0, 0,
x + glyphs[idx].left, y - glyphs[idx].top,
glyphs[idx].img_w, glyphs[idx].img_h
);
if (cursor && *cursor == i)
XDrawLine(display, draw_ctx->window, draw_ctx->gc, x, caret_y1, x, caret_y2);
x += FONT_WIDTH(glyphs[0]);
}
}
if (cursor && *cursor == length)
XDrawLine(display, draw_ctx->window, draw_ctx->gc, x, caret_y1, x, caret_y2);
}
void draw_menu(Menu_View *view, Listing *list, Settings *config, Glyph *renders, Draw_Info *draw_ctx, int y) {
int results_font_h = FONT_HEIGHT(renders[RES_OFFSET]);
int sel_offset = results_font_h * BELOW_CURSOR_RATIO;
view->visible = (draw_ctx->window_h - BORDER_PX - y) / results_font_h + 1;
for (int i = view->top; i < view->top + view->visible && i < view->n_items; i++) {
int idx = view->menu[i];
char *entry = list->table[idx];
int len = strlen(entry);
int offset = 0;
int type = list->stats[idx].st_mode & S_IFMT;
if (type == S_IFDIR)
offset = 2 * N_CHARS;
if (type == S_IFLNK)
offset += N_CHARS;
if (i == view->selected) {
XSetForeground(display, draw_ctx->gc, config->selected_color);
XFillRectangle(
display, draw_ctx->window, draw_ctx->gc,
BORDER_PX/2, y - results_font_h + sel_offset,
draw_ctx->window_w - BORDER_PX, results_font_h
);
XSetForeground(display, draw_ctx->gc, config->caret_color);
draw_string(entry, len, 0, NULL, BORDER_PX, y, draw_ctx, &renders[SEL_OFFSET + offset]);
}
else
draw_string(entry, len, 0, NULL, BORDER_PX, y, draw_ctx, &renders[RES_OFFSET + offset]);
y += results_font_h;
}
}
int run_gui(Settings *config, Screen_Info *screen_info, Glyph *renders, char *textbox, int textbox_len, char *error_msg) {
if (error_msg) {
XSync(display, true);
XFlush(display);
memset(textbox, 0, textbox_len);
}
Draw_Info draw_ctx;
create_window(config, screen_info, &draw_ctx);
XStoreName(display, draw_ctx.window, WINDOW_TITLE);
remove_window_border(display, draw_ctx.window);
XSelectInput(display, draw_ctx.window, ExposureMask | FocusChangeMask | KeyPressMask | KeyReleaseMask);
draw_ctx.gc = XCreateGC(display, draw_ctx.window, 0, NULL);
XSetForeground(display, draw_ctx.gc, config->caret_color);
XSetBackground(display, draw_ctx.gc, config->back_color);
for (int i = 0; i < N_RENDERS; i++) {
if (renders[i].data)
make_32bpp_ximage(
display, draw_ctx.visual,
renders[i].data, renders[i].img_w, renders[i].img_h,
(XImage*)&renders[i].ximage
);
}
Atom delete_msg = XInternAtom(display, "WM_DELETE_WINDOW", false);
XSetWMProtocols(display, draw_ctx.window, &delete_msg, 1);
XMapRaised(display, draw_ctx.window);
skip_taskbar(display, draw_ctx.window, RootWindow(display, screen_info->idx));
int cursor = 0;
int menu[MENU_SIZE] = {0};
Menu_View view = {
.menu = menu,
.n_items = 0,
.selected = -1,
.top = 0
};
Listing listing;
char key_buf[10] = {0};
bool modifier_held = false;
bool run_command = false;
bool done = false;
while (!done) {
XEvent event;
XNextEvent(display, &event);
switch (event.type) {
case Expose:
{
int x = BORDER_PX;
int font_h = FONT_HEIGHT(renders[BAR_OFFSET]);
int y = font_h * VERT_GAP_RATIO;
int caret_y1 = y - font_h * ABOVE_CURSOR_RATIO;
int caret_y2 = y + font_h * BELOW_CURSOR_RATIO;
XDrawLine(display, draw_ctx.window, draw_ctx.gc, x, caret_y1, x, caret_y2);
if (error_msg)
draw_string(
error_msg, strlen(error_msg),
0, NULL,
BORDER_PX, draw_ctx.window_h - BORDER_PX,
&draw_ctx,
&renders[ERR_OFFSET]
);
break;
}
case FocusOut:
if (event.xfocus.mode != NotifyUngrab)
done = true;
break;
case ClientMessage:
if (event.xclient.data.l[0] == delete_msg)
done = true;
break;
case KeyRelease:
if (IsModifierKey(XLookupKeysym((XKeyEvent*)&event, 0)))
modifier_held = false;
break;
case KeyPress:
{
int len = strlen(textbox);
KeySym key;
int input_len = XLookupString((XKeyEvent*)&event, key_buf, 10, &key, 0);
if (IsModifierKey(key))
modifier_held = true;
int up_delta = 0;
int down_delta = 0;
switch (key) {
case XK_Escape:
done = true;
break;
case XK_Return:
if (view.selected < 0) {
done = true;
run_command = true;
}
break;
case XK_Up:
up_delta = 1;
break;
case XK_Page_Up:
up_delta = view.visible;
break;
case XK_Down:
down_delta = 1;
break;
case XK_Page_Down:
down_delta = view.visible;
break;
case XK_Left:
if (view.selected < 0)
cursor = cursor > 0 ? cursor-1 : 0;
break;
case XK_Right:
if (view.selected < 0)
cursor = cursor < len ? cursor+1 : len;
break;
case XK_Home:
cursor = 0;
break;
case XK_End:
cursor = len;
break;
case XK_BackSpace:
remove_char(textbox, len, cursor);
if (cursor > 0) cursor--;
case XK_Delete:
if (key == XK_Delete)
remove_char(textbox, len, cursor + 1);
view.top = 0;
view.selected = -1;
break;
}
if (done)
break;
if (up_delta) {
int pos = view.selected - up_delta;
view.selected = pos > -1 ? pos : -1;
if (view.selected >= 0 && view.selected < view.top)
view.top = view.selected;
if (view.selected < 0)
view.top = 0;
}
else if (down_delta) {
int pos = view.selected + down_delta;
view.selected = pos < view.n_items-1 ? pos : view.n_items-1;
if (view.selected > view.top + view.visible-1)
view.top = view.selected - (view.visible-1);
}
int add_len = 0;
// shift+tab or modifier+left
if (len > 0 && (key == XK_ISO_Left_Tab || (key == XK_Left && modifier_held))) {
int idx = len - 1;
do {
textbox[idx] = 0;
idx--;
} while (idx >= 0 && textbox[idx] != '/');
add_len = idx - (len-1);
}
if (len < textbox_len - input_len) {
if (key_buf[0] == '~' &&
(cursor == 0 || (cursor >= 2 && textbox[cursor-1] == ' ' && textbox[cursor-2] != '\\'))
) {
key_buf[1] = '/';
input_len++;
}
add_len += insert_chars(textbox, len, key_buf, input_len, cursor);
}
if (add_len) {
view.top = 0;
view.selected = -1;
cursor += add_len;
}
char *word = NULL;
int word_len = 0;
int trailing = 0;
memset(&listing, 0, sizeof(Listing));
bool is_command = enumerate_directory(textbox, cursor, &word, &word_len, &trailing, &listing);
char *match = NULL;
int match_len = 0;
if (key == XK_Tab && view.selected < 0) {
match = find_completeable_span(&listing, word, word_len, trailing, &match_len);
}
else if (view.selected >= 0 && (key == XK_Tab || key == XK_Right || key == XK_Return) && listing.n_entries > 0) {
match = listing.table[view.menu[view.selected]];
match_len = strlen(match);
}
if (match) {
bool folder_completion = view.n_items == 1 || view.selected >= 0;
trailing = complete(word, &word_len, match, match_len, trailing, folder_completion);
if (key == XK_Return) {
done = true;
run_command = true;
break;
}
if (!trailing)
enumerate_directory(textbox, cursor, &word, &word_len, NULL, &listing);
cursor = &word[word_len] - textbox;
view.selected = -1;
view.top = 0;
}
view.n_items = 0;
bool show_menu = listing.n_entries && !(is_command && trailing == 0);
if (trailing == 0 && listing.n_entries > 0) {
view.menu = listing.index;
view.n_items = listing.n_entries;
}
else {
view.menu = menu;
if (show_menu) {
for (int i = 0; i < listing.n_entries && view.n_items < MENU_SIZE; i++) {
int idx = listing.index[i];
if (!difference_ignoring_backslashes(listing.table[idx], word, word_len, trailing))
view.menu[view.n_items++] = idx;
}
}
}
XClearArea(display, draw_ctx.window, 0, 0, draw_ctx.window_w, draw_ctx.window_h, false);
int search_font_h = FONT_HEIGHT(renders[BAR_OFFSET]);
int gap = search_font_h * VERT_GAP_RATIO;
int max_chars = (draw_ctx.window_w - BORDER_PX) / FONT_WIDTH(renders[BAR_OFFSET]);
int offset = (cursor >= max_chars) ? cursor - (max_chars-1) : 0;
draw_string(textbox, -1, offset, &cursor, BORDER_PX, gap, &draw_ctx, &renders[BAR_OFFSET]);
if (show_menu)
draw_menu(&view, &listing, config, renders, &draw_ctx, gap * 2);
break;
}
}
}
XFreeGC(display, draw_ctx.gc);
XDestroyWindow(display, draw_ctx.window);
return run_command ? STATUS_COMMAND : STATUS_EXIT;
}
bool open_display(int screen_idx, Screen_Info *screen_info) {
display = XOpenDisplay(NULL);
if (!display)
return false;
Screen *screen = ScreenOfDisplay(display, screen_idx);
if (!screen)
return false;
screen_info->idx = screen_idx;
screen_info->w = WidthOfScreen(screen);
screen_info->h = HeightOfScreen(screen);
screen_info->dpi_w = (float)screen_info->w * 25.4 / (float)WidthMMOfScreen(screen);
screen_info->dpi_h = (float)screen_info->h * 25.4 / (float)HeightMMOfScreen(screen);
return true;
}
void close_display() {
if (display) {
XCloseDisplay(display);
display = NULL;
}
}