fork with minor changes https://dunst-project.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
dunst/src/x11/x.c

925 lines
29 KiB

/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */
#include "x.h"
#include <assert.h>
#include <cairo.h>
#include <cairo-xlib.h>
#include <glib-object.h>
#include <locale.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <X11/extensions/shape.h>
#include <X11/Xatom.h>
#include <X11/X.h>
#include <X11/XKBlib.h>
#include <X11/Xlib.h>
#include <X11/Xresource.h>
#include <X11/Xutil.h>
#include "../dbus.h"
#include "../draw.h"
#include "../dunst.h"
#include "../log.h"
#include "../markup.h"
#include "../menu.h"
#include "../notification.h"
#include "../queues.h"
#include "../settings.h"
#include "../utils.h"
#include "screen.h"
#define WIDTH 400
#define HEIGHT 400
struct window_x11 {
Window xwin;
cairo_surface_t *root_surface;
cairo_t *c_ctx;
GSource *esrc;
int cur_screen;
bool visible;
struct dimensions dim;
};
struct x11_source {
GSource source;
struct window_x11 *win;
};
struct x_context xctx;
bool dunst_grab_errored = false;
static bool fullscreen_last = false;
static void XRM_update_db(void);
static void x_shortcut_init(struct keyboard_shortcut *ks);
static int x_shortcut_grab(struct keyboard_shortcut *ks);
static void x_shortcut_ungrab(struct keyboard_shortcut *ks);
/* FIXME refactor setup teardown handlers into one setup and one teardown */
static void x_shortcut_setup_error_handler(void);
static int x_shortcut_tear_down_error_handler(void);
static void setopacity(Window win, unsigned long opacity);
static void x_handle_click(XEvent ev);
static void x_win_move(struct window_x11 *win, int x, int y, int width, int height)
{
/* move and resize */
if (x != win->dim.x || y != win->dim.y) {
XMoveWindow(xctx.dpy, win->xwin, x, y);
win->dim.x = x;
win->dim.y = y;
}
if (width != win->dim.w || height != win->dim.h) {
XResizeWindow(xctx.dpy, win->xwin, width, height);
win->dim.h = height;
win->dim.w = width;
}
}
static void x_win_round_corners(struct window_x11 *win, const int rad)
{
const int width = win->dim.w;
const int height = win->dim.h;
unsigned const int coords[] = {
0 + rad,
width - rad,
height - rad,
};
Pixmap mask;
cairo_surface_t * cxbm;
cairo_t * cr;
Screen * scr;
mask = XCreatePixmap(xctx.dpy, win->xwin, width, height, 1);
scr = ScreenOfDisplay(xctx.dpy, win->cur_screen);
cxbm = cairo_xlib_surface_create_for_bitmap(xctx.dpy, mask, scr, width, height);
cr = cairo_create(cxbm);
cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_set_source_rgba(cr, 0, 0, 0, 0);
cairo_paint(cr);
cairo_set_source_rgba(cr, 1, 1, 1, 1);
cairo_new_path(cr);
cairo_arc(cr, coords[0], coords[0], rad, M_PI, -M_PI_2);
cairo_arc(cr, coords[1], coords[0], rad, -M_PI_2, 0);
cairo_arc(cr, coords[1], coords[2], rad, 0, M_PI_2);
cairo_arc(cr, coords[0], coords[2], rad, M_PI_2, M_PI);
cairo_close_path(cr);
cairo_fill(cr);
cairo_show_page(cr);
cairo_destroy(cr);
cairo_surface_flush(cxbm);
cairo_surface_destroy(cxbm);
XShapeCombineMask(xctx.dpy, win->xwin, ShapeBounding, 0, 0, mask, ShapeSet);
XFreePixmap(xctx.dpy, mask);
XShapeSelectInput(xctx.dpy,
win->xwin, ShapeNotifyMask);
}
void x_display_surface(cairo_surface_t *srf, struct window_x11 *win, const struct dimensions *dim)
{
x_win_move(win, dim->x, dim->y, dim->w, dim->h);
cairo_xlib_surface_set_size(win->root_surface, dim->w, dim->h);
XClearWindow(xctx.dpy, win->xwin);
XFlush(xctx.dpy);
cairo_set_source_surface(win->c_ctx, srf, 0, 0);
cairo_paint(win->c_ctx);
cairo_show_page(win->c_ctx);
if (settings.corner_radius != 0)
x_win_round_corners(win, dim->corner_radius);
XFlush(xctx.dpy);
}
bool x_win_visible(struct window_x11 *win)
{
return win->visible;
}
cairo_t* x_win_get_context(struct window_x11 *win)
{
return win->c_ctx;
}
static void setopacity(Window win, unsigned long opacity)
{
Atom _NET_WM_WINDOW_OPACITY =
XInternAtom(xctx.dpy, "_NET_WM_WINDOW_OPACITY", false);
XChangeProperty(xctx.dpy,
win,
_NET_WM_WINDOW_OPACITY,
XA_CARDINAL,
32,
PropModeReplace,
(unsigned char *)&opacity,
1L);
}
/*
* Returns the modifier which is NumLock.
*/
static KeySym x_numlock_mod(void)
{
static KeyCode nl = 0;
KeySym sym = 0;
XModifierKeymap *map = XGetModifierMapping(xctx.dpy);
if (!nl)
nl = XKeysymToKeycode(xctx.dpy, XStringToKeysym("Num_Lock"));
for (int mod = 0; mod < 8; mod++) {
for (int j = 0; j < map->max_keypermod; j++) {
if (map->modifiermap[mod*map->max_keypermod+j] == nl) {
/* In theory, one could use `1 << mod`, but this
* could count as 'using implementation details',
* so use this large switch. */
switch (mod) {
case ShiftMapIndex:
sym = ShiftMask;
goto end;
case LockMapIndex:
sym = LockMask;
goto end;
case ControlMapIndex:
sym = ControlMask;
goto end;
case Mod1MapIndex:
sym = Mod1Mask;
goto end;
case Mod2MapIndex:
sym = Mod2Mask;
goto end;
case Mod3MapIndex:
sym = Mod3Mask;
goto end;
case Mod4MapIndex:
sym = Mod4Mask;
goto end;
case Mod5MapIndex:
sym = Mod5Mask;
goto end;
}
}
}
}
end:
XFreeModifiermap(map);
return sym;
}
/*
* Helper function to use glib's mainloop mechanic
* with Xlib
*/
gboolean x_mainloop_fd_prepare(GSource *source, gint *timeout)
{
if (timeout)
*timeout = -1;
return false;
}
/*
* Helper function to use glib's mainloop mechanic
* with Xlib
*/
gboolean x_mainloop_fd_check(GSource *source)
{
return XPending(xctx.dpy) > 0;
}
/*
* Main Dispatcher for XEvents
*/
gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
{
struct window_x11 *win = ((struct x11_source*) source)->win;
bool fullscreen_now;
struct screen_info *scr;
XEvent ev;
unsigned int state;
while (XPending(xctx.dpy) > 0) {
XNextEvent(xctx.dpy, &ev);
switch (ev.type) {
case Expose:
LOG_D("XEvent: processing 'Expose'");
if (ev.xexpose.count == 0 && win->visible) {
draw();
}
break;
case ButtonRelease:
LOG_D("XEvent: processing 'ButtonRelease'");
if (ev.xbutton.window == win->xwin) {
x_handle_click(ev);
wake_up();
}
break;
case KeyPress:
LOG_D("XEvent: processing 'KeyPress'");
state = ev.xkey.state;
/* NumLock is also encoded in the state. Remove it. */
state &= ~x_numlock_mod();
if (settings.close_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.close_ks.sym
&& settings.close_ks.mask == state) {
const GList *displayed = queues_get_displayed();
if (displayed && displayed->data) {
queues_notification_close(displayed->data, REASON_USER);
wake_up();
}
}
if (settings.history_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.history_ks.sym
&& settings.history_ks.mask == state) {
queues_history_pop();
wake_up();
}
if (settings.close_all_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.close_all_ks.sym
&& settings.close_all_ks.mask == state) {
queues_history_push_all();
wake_up();
}
if (settings.context_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.context_ks.sym
&& settings.context_ks.mask == state) {
context_menu();
wake_up();
}
break;
case CreateNotify:
LOG_D("XEvent: processing 'CreateNotify'");
if (win->visible &&
ev.xcreatewindow.override_redirect == 0)
XRaiseWindow(xctx.dpy, win->xwin);
break;
case PropertyNotify:
if (ev.xproperty.atom == XA_RESOURCE_MANAGER) {
LOG_D("XEvent: processing PropertyNotify for Resource manager");
XRM_update_db();
screen_dpi_xft_cache_purge();
if (win->visible) {
draw();
}
break;
}
/* Explicitly fallthrough. Other PropertyNotify events, e.g. catching
* _NET_WM get handled in the Focus(In|Out) section */
case ConfigureNotify:
case FocusIn:
case FocusOut:
LOG_D("XEvent: Checking for active screen changes");
fullscreen_now = have_fullscreen_window();
scr = get_active_screen();
if (fullscreen_now != fullscreen_last) {
fullscreen_last = fullscreen_now;
wake_up();
} else if ( settings.f_mode != FOLLOW_NONE
/* Ignore PropertyNotify, when we're still on the
* same screen. PropertyNotify is only necessary
* to detect a focus change to another screen
*/
&& win->visible
&& scr->id != win->cur_screen) {
draw();
win->cur_screen = scr->id;
}
break;
default:
if (!screen_check_event(&ev)) {
LOG_D("XEvent: Ignoring '%d'", ev.type);
}
break;
}
}
return G_SOURCE_CONTINUE;
}
/*
* Check whether the user is currently idle.
*/
bool x_is_idle(void)
{
XScreenSaverQueryInfo(xctx.dpy, DefaultRootWindow(xctx.dpy),
xctx.screensaver_info);
if (settings.idle_threshold == 0) {
return false;
}
return xctx.screensaver_info->idle > settings.idle_threshold / 1000;
}
/* TODO move to x_mainloop_* */
/*
* Handle incoming mouse click events
*/
static void x_handle_click(XEvent ev)
{
enum mouse_action *acts;
switch (ev.xbutton.button) {
case Button1:
acts = settings.mouse_left_click;
break;
case Button2:
acts = settings.mouse_middle_click;
break;
case Button3:
acts = settings.mouse_right_click;
break;
default:
LOG_W("Unsupported mouse button: '%d'", ev.xbutton.button);
return;
}
for (int i = 0; acts[i]; i++) {
enum mouse_action act = acts[i];
if (act == MOUSE_CLOSE_ALL) {
queues_history_push_all();
return;
}
if (act == MOUSE_DO_ACTION || act == MOUSE_CLOSE_CURRENT) {
int y = settings.separator_height;
struct notification *n = NULL;
int first = true;
for (const GList *iter = queues_get_displayed(); iter;
iter = iter->next) {
n = iter->data;
if (ev.xbutton.y > y && ev.xbutton.y < y + n->displayed_height)
break;
y += n->displayed_height + settings.separator_height;
if (first)
y += settings.frame_width;
}
if (n) {
if (act == MOUSE_CLOSE_CURRENT)
queues_notification_close(n, REASON_USER);
else
notification_do_action(n);
}
}
}
}
void x_free(void)
{
if (xctx.screensaver_info)
XFree(xctx.screensaver_info);
if (xctx.dpy)
XCloseDisplay(xctx.dpy);
}
static int XErrorHandlerDB(Display *display, XErrorEvent *e)
{
char err_buf[BUFSIZ];
XGetErrorText(display, e->error_code, err_buf, BUFSIZ);
LOG_W("%s", err_buf);
return 0;
}
static void XRM_update_db(void)
{
XrmDatabase db;
XTextProperty prop;
Window root;
// We shouldn't destroy the first DB coming
// from the display object itself
static bool runonce = false;
XFlush(xctx.dpy);
XSetErrorHandler(XErrorHandlerDB);
root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy));
XLockDisplay(xctx.dpy);
if (XGetTextProperty(xctx.dpy, root, &prop, XA_RESOURCE_MANAGER)) {
if (runonce) {
db = XrmGetDatabase(xctx.dpy);
XrmDestroyDatabase(db);
}
db = XrmGetStringDatabase((const char*)prop.value);
XrmSetDatabase(xctx.dpy, db);
}
XUnlockDisplay(xctx.dpy);
runonce = true;
XFlush(xctx.dpy);
XSync(xctx.dpy, false);
XSetErrorHandler(NULL);
}
/*
* Setup X11 stuff
*/
void x_setup(void)
{
/* initialize xctx.dc, font, keyboard, colors */
if (!setlocale(LC_CTYPE, "") || !XSupportsLocale())
LOG_W("No locale support");
if (!(xctx.dpy = XOpenDisplay(NULL))) {
DIE("Cannot open X11 display.");
}
x_shortcut_init(&settings.close_ks);
x_shortcut_init(&settings.close_all_ks);
x_shortcut_init(&settings.history_ks);
x_shortcut_init(&settings.context_ks);
x_shortcut_grab(&settings.close_ks);
x_shortcut_ungrab(&settings.close_ks);
x_shortcut_grab(&settings.close_all_ks);
x_shortcut_ungrab(&settings.close_all_ks);
x_shortcut_grab(&settings.history_ks);
x_shortcut_ungrab(&settings.history_ks);
x_shortcut_grab(&settings.context_ks);
x_shortcut_ungrab(&settings.context_ks);
xctx.screensaver_info = XScreenSaverAllocInfo();
init_screens();
x_shortcut_grab(&settings.history_ks);
XrmInitialize();
}
struct geometry x_parse_geometry(const char *geom_str)
{
assert(geom_str);
struct geometry geometry = { 0 };
if (geom_str[0] == '-') {
geometry.negative_width = true;
geom_str++;
} else {
geometry.negative_width = false;
}
int mask = XParseGeometry(geom_str,
&geometry.x, &geometry.y,
&geometry.w, &geometry.h);
geometry.width_set = mask & WidthValue;
geometry.negative_x = mask & XNegative;
geometry.negative_y = mask & YNegative;
return geometry;
}
static void x_set_wm(Window win)
{
Atom data[2];
/* set window title */
char *title = settings.title != NULL ? settings.title : "Dunst";
Atom _net_wm_title =
XInternAtom(xctx.dpy, "_NET_WM_NAME", false);
XStoreName(xctx.dpy, win, title);
XChangeProperty(xctx.dpy,
win,
_net_wm_title,
XInternAtom(xctx.dpy, "UTF8_STRING", false),
8,
PropModeReplace,
(unsigned char *)title,
strlen(title));
/* set window class */
char *class = settings.class != NULL ? settings.class : "Dunst";
XClassHint classhint = { class, "Dunst" };
XSetClassHint(xctx.dpy, win, &classhint);
/* set window type */
Atom net_wm_window_type =
XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE", false);
data[0] = XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE_NOTIFICATION", false);
data[1] = XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE_UTILITY", false);
XChangeProperty(xctx.dpy,
win,
net_wm_window_type,
XA_ATOM,
32,
PropModeReplace,
(unsigned char *)data,
2L);
/* set state above */
Atom net_wm_state =
XInternAtom(xctx.dpy, "_NET_WM_STATE", false);
data[0] = XInternAtom(xctx.dpy, "_NET_WM_STATE_ABOVE", false);
XChangeProperty(xctx.dpy, win, net_wm_state, XA_ATOM, 32,
PropModeReplace, (unsigned char *) data, 1L);
}
GSource* x_win_reg_source(struct window_x11 *win)
{
// Static is necessary here because glib keeps the pointer and we need
// to keep the reference alive.
static GSourceFuncs xsrc_fn = {
x_mainloop_fd_prepare,
x_mainloop_fd_check,
x_mainloop_fd_dispatch,
NULL,
NULL,
NULL
};
struct x11_source *xsrc = (struct x11_source*) g_source_new(&xsrc_fn,
sizeof(struct x11_source));
xsrc->win = win;
g_source_add_unix_fd((GSource*) xsrc, xctx.dpy->fd, G_IO_IN | G_IO_HUP | G_IO_ERR);
g_source_attach((GSource*) xsrc, NULL);
return (GSource*)xsrc;
}
/*
* Setup the window
*/
struct window_x11 *x_win_create(void)
{
struct window_x11 *win = g_malloc0(sizeof(struct window_x11));
Window root;
int scr_n;
int depth;
Visual * vis;
XVisualInfo vi;
XSetWindowAttributes wa;
scr_n = DefaultScreen(xctx.dpy);
root = RootWindow(xctx.dpy, scr_n);
if (XMatchVisualInfo(xctx.dpy, scr_n, 32, TrueColor, &vi))
{
vis = vi.visual;
depth = vi.depth;
}
else
{
vis = DefaultVisual(xctx.dpy, scr_n);
depth = DefaultDepth(xctx.dpy, scr_n);
}
wa.override_redirect = true;
wa.background_pixmap = None;
wa.background_pixel = 0;
wa.border_pixel = 0;
wa.colormap = XCreateColormap(xctx.dpy, root, vis, AllocNone);
wa.event_mask =
ExposureMask | KeyPressMask | VisibilityChangeMask |
ButtonReleaseMask | FocusChangeMask| StructureNotifyMask;
struct screen_info *scr = get_active_screen();
win->xwin = XCreateWindow(xctx.dpy,
root,
scr->x,
scr->y,
scr->w,
1,
0,
depth,
CopyFromParent,
vis,
CWOverrideRedirect | CWBackPixmap | CWBackPixel | CWBorderPixel | CWColormap | CWEventMask,
&wa);
x_set_wm(win->xwin);
settings.transparency =
settings.transparency > 100 ? 100 : settings.transparency;
setopacity(win->xwin,
(unsigned long)((100 - settings.transparency) *
(0xffffffff / 100)));
win->root_surface = cairo_xlib_surface_create(xctx.dpy, win->xwin,
vis,
WIDTH, HEIGHT);
win->c_ctx = cairo_create(win->root_surface);
win->esrc = x_win_reg_source(win);
/* SubstructureNotifyMask is required for receiving CreateNotify events
* in order to raise the window when something covers us. See #160
*
* PropertyChangeMask is requred for getting screen change events when follow_mode != none
* and it's also needed to receive
* XA_RESOURCE_MANAGER events to update the dpi when
* the xresource value is updated
*/
long root_event_mask = SubstructureNotifyMask | PropertyChangeMask;
if (settings.f_mode != FOLLOW_NONE) {
root_event_mask |= FocusChangeMask;
}
XSelectInput(xctx.dpy, root, root_event_mask);
return win;
}
void x_win_destroy(struct window_x11 *win)
{
g_source_destroy(win->esrc);
g_source_unref(win->esrc);
cairo_destroy(win->c_ctx);
cairo_surface_destroy(win->root_surface);
XDestroyWindow(xctx.dpy, win->xwin);
g_free(win);
}
/*
* Show the window and grab shortcuts.
*/
void x_win_show(struct window_x11 *win)
{
/* window is already mapped or there's nothing to show */
if (win->visible)
return;
x_shortcut_grab(&settings.close_ks);
x_shortcut_grab(&settings.close_all_ks);
x_shortcut_grab(&settings.context_ks);
x_shortcut_setup_error_handler();
XGrabButton(xctx.dpy,
AnyButton,
AnyModifier,
win->xwin,
false,
(ButtonPressMask|ButtonReleaseMask),
GrabModeAsync,
GrabModeSync,
None,
None);
if (x_shortcut_tear_down_error_handler()) {
LOG_W("Unable to grab mouse button(s).");
}
XMapRaised(xctx.dpy, win->xwin);
win->visible = true;
}
/*
* Hide the window and ungrab unused keyboard_shortcuts
*/
void x_win_hide(struct window_x11 *win)
{
ASSERT_OR_RET(win->visible,);
x_shortcut_ungrab(&settings.close_ks);
x_shortcut_ungrab(&settings.close_all_ks);
x_shortcut_ungrab(&settings.context_ks);
XUngrabButton(xctx.dpy, AnyButton, AnyModifier, win->xwin);
XUnmapWindow(xctx.dpy, win->xwin);
XFlush(xctx.dpy);
win->visible = false;
}
/*
* Parse a string into a modifier mask.
*/
KeySym x_shortcut_string_to_mask(const char *str)
{
if (STR_EQ(str, "ctrl")) {
return ControlMask;
} else if (STR_EQ(str, "mod4")) {
return Mod4Mask;
} else if (STR_EQ(str, "mod3")) {
return Mod3Mask;
} else if (STR_EQ(str, "mod2")) {
return Mod2Mask;
} else if (STR_EQ(str, "mod1")) {
return Mod1Mask;
} else if (STR_EQ(str, "shift")) {
return ShiftMask;
} else {
LOG_W("Unknown Modifier: '%s'", str);
return 0;
}
}
/*
* Error handler for grabbing mouse and keyboard errors.
*/
static int GrabXErrorHandler(Display *display, XErrorEvent *e)
{
dunst_grab_errored = true;
char err_buf[BUFSIZ];
XGetErrorText(display, e->error_code, err_buf, BUFSIZ);
if (e->error_code != BadAccess) {
DIE("%s", err_buf);
} else {
LOG_W("%s", err_buf);
}
return 0;
}
/*
* Setup the Error handler.
*/
static void x_shortcut_setup_error_handler(void)
{
dunst_grab_errored = false;
XFlush(xctx.dpy);
XSetErrorHandler(GrabXErrorHandler);
}
/*
* Tear down the Error handler.
*/
static int x_shortcut_tear_down_error_handler(void)
{
XFlush(xctx.dpy);
XSync(xctx.dpy, false);
XSetErrorHandler(NULL);
return dunst_grab_errored;
}
/*
* Grab the given keyboard shortcut.
*/
static int x_shortcut_grab(struct keyboard_shortcut *ks)
{
ASSERT_OR_RET(ks->is_valid, 1);
Window root;
root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy));
x_shortcut_setup_error_handler();
if (ks->is_valid) {
XGrabKey(xctx.dpy,
ks->code,
ks->mask,
root,
true,
GrabModeAsync,
GrabModeAsync);
XGrabKey(xctx.dpy,
ks->code,
ks->mask | x_numlock_mod(),
root,
true,
GrabModeAsync,
GrabModeAsync);
}
if (x_shortcut_tear_down_error_handler()) {
LOG_W("Unable to grab key '%s'.", ks->str);
ks->is_valid = false;
return 1;
}
return 0;
}
/*
* Ungrab the given keyboard shortcut.
*/
static void x_shortcut_ungrab(struct keyboard_shortcut *ks)
{
Window root;
root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy));
if (ks->is_valid) {
XUngrabKey(xctx.dpy, ks->code, ks->mask, root);
XUngrabKey(xctx.dpy, ks->code, ks->mask | x_numlock_mod(), root);
}
}
/*
* Initialize the keyboard shortcut.
*/
static void x_shortcut_init(struct keyboard_shortcut *ks)
{
ASSERT_OR_RET(ks && ks->str,);
if (STR_EQ(ks->str, "none") || (STR_EQ(ks->str, ""))) {
ks->is_valid = false;
return;
}
char *str = g_strdup(ks->str);
char *str_begin = str;
while (strchr(str, '+')) {
char *mod = str;
while (*str != '+')
str++;
*str = '\0';
str++;
g_strchomp(mod);
ks->mask = ks->mask | x_shortcut_string_to_mask(mod);
}
g_strstrip(str);
ks->sym = XStringToKeysym(str);
/* find matching keycode for ks->sym */
int min_keysym, max_keysym;
XDisplayKeycodes(xctx.dpy, &min_keysym, &max_keysym);
ks->code = NoSymbol;
for (int i = min_keysym; i <= max_keysym; i++) {
if (XkbKeycodeToKeysym(xctx.dpy, i, 0, 0) == ks->sym
|| XkbKeycodeToKeysym(xctx.dpy, i, 0, 1) == ks->sym) {
ks->code = i;
break;
}
}
if (ks->sym == NoSymbol || ks->code == NoSymbol) {
LOG_W("Unknown keyboard shortcut: '%s'", ks->str);
ks->is_valid = false;
} else {
ks->is_valid = true;
}
g_free(str_begin);
}
/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */