--Xlib binding. --Written by Cosmin Apreutesei. Public Domain. --All observations in the comments were made on an Ubuntu 10 Unity WM. --Since all WMs are hopelessly broken (because X is hopelessly broken), --don't expect these observations to hold on other WMs. local ffi = require'ffi' assert(ffi.os == 'Linux', 'platform not Linux') local bit = require'bit' local glue = require'glue' local X = require'xlib_h' --X is the macro namespace local _ = require'xlib_xkblib_h' --local _ = require'xlib_xcursor_h' --not yet needed local C = ffi.load'X11.so.6' --Xlib core local XC = ffi.load'Xext.so.6' --Xlib core extensions local CC = ffi.load'Xcursor.so.1' --Xcursor helper library local M = {C = C, XC = XC, X = X} local print = print --bit arrays over byte arrays ------------------------------------------------ local band, bor, shl, shr = bit.band, bit.bor, bit.lshift, bit.rshift local function getbit(b, bits) return band(bits[shr(b, 3)], shl(1, band(b, 7))) ~= 0 end local function setbit(b, bits) bits[shr(b, 3)] = bor(bits[shr(b, 3)], shl(1, band(b, 7))) end M.getbit = getbit M.setbit = setbit --conversion helpers --------------------------------------------------------- local function ptr(p, free) --NULL -> nil conversion and optional gc hooking. return p ~= nil and ffi.gc(p, free) or nil end --XIDs can't exceed 32bit so we convert them to Lua numbers on x64 --to be able to use them as table keys directly. Oh, and 0 -> nil. local function xid(x) return x ~= 0 and tonumber(x) or nil end M.ptr = ptr M.xid = xid --masked C struct helpers ---------------------------------------------------- --set a masked C struct's fields based on a value table and a masks table. local function maskedset(ct, t, masks) local mask = 0 if t then for field, val in pairs(t) do local maskbit = masks[field] if maskbit then mask = bor(mask, maskbit) ct[field] = val end end end return mask end --read the set values of a masked C struct into a Lua table. local function maskedget(ct, mask, masks) local t = {} for field, maskbit in pairs(masks) do if band(mask, maskbit) ~= 0 then t[field] = ct[field] end end return t end --glibc cdefs for setting _NET_WM_PID and WM_CLIENT_MACHINE ------------------ ffi.cdef[[ int xlib_getpid() asm("getpid"); int xlib_gethostname(char *name, size_t len) asm("gethostname"); typedef struct { char sysname[65]; char nodename[65]; char release[65]; char version[65]; char machine[65]; char __domainname[65]; } xlib_utsname; int xlib_uname(xlib_utsname* buf) asm("uname"); ]] --glibc cdefs for waiting on a socket with a timeout ------------------------- ffi.cdef[[ typedef struct { uint8_t bits[128]; // 1024 bits } xlib_fd_set; typedef struct { long int tv_sec; long int tv_usec; } xlib_timeval; int xlib_select(int, xlib_fd_set*, xlib_fd_set*, xlib_fd_set*, xlib_timeval*) asm("select"); ]] local function FD_ZERO(fds) ffi.fill(fds, ffi.sizeof(fds)) end local function FD_ISSET(d, set) return getbit(d, set.bits) end local function FD_SET(d, set) assert(d <= 1024) setbit(d, set.bits) end local timeval, fds local function select_fd(fd, timeout) --returns true if fd has data, false if timed out timeval = timeval or ffi.new'xlib_timeval' timeval.tv_sec = timeout timeval.tv_usec = (timeout - math.floor(timeout)) * 10^6 fds = fds or ffi.new'xlib_fd_set' FD_ZERO(fds) FD_SET(fd, fds) assert(C.xlib_select(fd + 1, fds, nil, nil, timeval) >= 0) return FD_ISSET(fd, fds) end --API ------------------------------------------------------------------------ function M.connect(...) local type, select, unpack, assert, error, ffi, bit, table, pairs, ipairs = type, select, unpack, assert, error, ffi, bit, table, pairs, ipairs local require, pcall, tonumber, setmetatable, rawget, glue, math = require, pcall, tonumber, setmetatable, rawget, glue, math local cast = ffi.cast local free = glue.free local xlib = glue.update({}, M) setfenv(1, xlib) --connection -------------------------------------------------------------- local c --Display* local fd --connection fd local screen_num --default screen number local screen --default Screen local cleanup = {} --disconnect handlers local errbuf local onerr = ffi.cast('XErrorHandler', function(c, e) local errbuf_sz = 256 errbuf = errbuf or ffi.new('char[?]', errbuf_sz) C.XGetErrorText(c, e.error_code, errbuf, errbuf_sz) error(ffi.string(errbuf), 2) end) local function connect(displayname) c = assert(ptr(C.XOpenDisplay(displayname))) C.XSetErrorHandler(onerr) fd = C.XConnectionNumber(c) screen_num = C.XDefaultScreen(c) screen = C.XScreenOfDisplay(c, screen_num) xlib.display = c xlib.display_fd = fd xlib.screen = screen xlib.screen_number = screen_num end function synchronize(enable) C.XSynchronize(c, enable or false) end function flush() C.XFlush(c) end function sync(discard_events) C.XSync(c, discard_events or 0) end --for submodules to add cleanup functions to execute on disconnect function gc(func) table.insert(cleanup, func) end function disconnect() for i,clean in ipairs(cleanup) do clean() end C.XCloseDisplay(c) c = nil --prevent use after disconnect end function setC(newC) C = newC end --server ------------------------------------------------------------------ --load the list of supported server extensions into a memoized hash extensions = glue.memoize(function() local nbuf = ffi.new'int[1]' local a = C.XListExtensions(c, nbuf) local n = nbuf[0] local t = {} for i=0,n-1 do local ext = ffi.string(a[i]) t[ext] = true end C.XFreeExtensionList(a) return t end) --check if the server has a specific extension function has_extension(s) return extensions()[s] end function version() return C.XProtocolVersion(c), C.XProtocolRevision(c) end --events ------------------------------------------------------------------ local e = ffi.new'XEvent' --poll or peek with or without blocking, returning an XEvent or nil. local function poll_func(XXEvent) return function(timeout) local n = tonumber(timeout) if n and n <= 0 then timeout = nil --negative timeout means do not block end if C.XPending(c) > 0 then XXEvent(c, e) C.XFilterEvent(e, 0) --add input method synthetic events return e end if timeout == true then XXEvent(c, e) --block indefinitely return e elseif timeout then if select_fd(fd, timeout) then --block with timeout XXEvent(c, e) return e end end end end poll = poll_func(C.XNextEvent) peek = poll_func(C.XPeekEvent) --poll or peek until a predicate is satisfied, returning true or nil on timeout. local function poll_until_func(poll) return function(func, timeout) while true do local e = poll(timeout) if not e then return end --timeout if func(e) then return true end --condition satisfied end end end poll_until = poll_until_func(poll) peek_until = poll_until_func(peek) --atoms ------------------------------------------------------------------- local atom_map = {} --{name = atom} local atom_revmap = {} --{atom = name} --lookup/intern an atom local mem_atom = glue.memoize(function(s) local atom = tonumber(C.XInternAtom(c, s, false)) atom_revmap[atom] = s return atom end, atom_map) function atom(s) if type(s) ~= 'string' then return s end --pass through return mem_atom(s) end --atom reverse lookup local mem_atom_name = glue.memoize(function(atom) local p = C.XGetAtomName(c, atom) if p == nil then return end local s = ffi.string(p) C.XFree(p) atom_map[s] = atom return s end, atom_revmap) function atom_name(atom) return mem_atom_name(tonumber(atom)) end --given a map {atom -> true} return the map {atom_name -> atom} function atom_names(t) local dt = {} for atom in pairs(t) do local name = atom_name(atom) if name then dt[name] = atom end end return dt end --screens ----------------------------------------------------------------- --NOTE: these days only one Screen is used and Xinerama is used to map --physical displays to this virtual screen. function screens() local n = C.XScreenCount(c) local i = -1 return function() i = i + 1 if i >= n then return end return i, C.XScreenOfDisplay(c, i) end end --window attributes buffer (used below) ----------------------------------- local masks = { background_pixmap = C.CWBackPixmap, background_pixel = C.CWBackPixel, border_pixmap = C.CWBorderPixmap, border_pixel = C.CWBorderPixel, bit_gravity = C.CWBitGravity, win_gravity = C.CWWinGravity, backing_store = C.CWBackingStore, backing_planes = C.CWBackingPlanes, backing_pixel = C.CWBackingPixel, save_under = C.CWSaveUnder, event_mask = C.CWEventMask, do_not_propagate_mask = C.CWDontPropagate, override_redirect = C.CWOverrideRedirect, colormap = C.CWColormap, cursor = C.CWCursor, } local abuf local function attr_buf(t) --attrs_t -> mask, attrs abuf = abuf or ffi.new'XSetWindowAttributes' return maskedset(abuf, t, masks), abuf end --constructors and destructors -------------------------------------------- --NOTE: t.x and t.y are ignored unless create_window() is followed --by set_wm_size_hints(win, {x = 0, y = 0}). Mental hospital-grade stuff. function create_window(t) local mask, attrs = attr_buf(t) return assert(xid(C.XCreateWindow(c, t.parent or screen.root, t.x or 0, --outer x (ignored) t.y or 0, --outer y (ignored) math.max(1, t.width), --inner width (prevent BadValue errors) math.max(1, t.height), --inner height (prevent BadValue errors) t.border_width or 0, --ignored t.depth or C.CopyFromParent, t.class or C.CopyFromParent, t.visual or nil, --means C.CopyFromParent mask, attrs))) end function destroy_window(win) C.XDestroyWindow(c, win) end function white_pixel(screen_num1) return C.XWhitePixel(c, screen_num1 or screen_num) end function black_pixel(screen_num1) return C.XBlackPixel(c, screen_num1 or screen_num) end function create_colormap(win, visual, alloc) return assert(xid(C.XCreateColormap(c, win, visual, alloc and C.AllocAll or C.AllocNone))) end function free_colormap(cmap) C.XFreeColormap(c, cmap) end function create_pixmap(win, w, h, depth) return assert(xid(C.XCreatePixmap(c, win, w, h, depth))) end function free_pixmap(pix) C.XFreePixmap(c, pix) end function create_gc(win, mask, values) return assert(ptr(C.XCreateGC(c, win, mask or 0, values))) end function free_gc(gc) C.XFreeGC(c, gc) end --window properties: encoding and decoding -------------------------------- function list_props(win) local nbuf = ffi.new'int[1]' local a = ptr(C.XListProperties(c, win, nbuf), C.XFree) local n = nbuf[0] local i = -1 return function() i = i + 1 if i >= n then if a then ffi.gc(a, nil) C.XFree(a) end return end return i+1, a[i] end end function delete_prop(win, prop) C.XDeleteProperty(c, win, atom(prop)) end function set_prop(win, prop, type, val, sz, format) C.XChangeProperty(c, win, atom(prop), atom(type), format or 32, C.PropModeReplace, ffi.cast('const unsigned char*', val), sz) end local reply_type = ffi.new'Atom[1]' local reply_format = ffi.new'int[1]' local nitems = ffi.new'unsigned long[1]' local bytes_after = ffi.new'unsigned long[1]' local data_ptr = ffi.new'unsigned char*[1]' function get_prop(win, prop, decode) local ret = C.XGetWindowProperty(c, win, atom(prop), 0, 2^22, false, C.AnyPropertyType, reply_type, reply_format, nitems, bytes_after, data_ptr) if ret == C.BadAtom or reply_type[0] == 0 then --property missing return end assert(ret == 0) if bytes_after[0] ~= 0 then C.XFree(data_ptr[0]) error('property value truncated', 2) end local data = data_ptr[0] local n = tonumber(nitems[0]) local ok, ret = pcall(decode, data, n, reply_format[0], reply_type[0]) C.XFree(data) if not ok then error(ret, 2) end return ret end function set_string_prop(win, prop, val) set_prop(win, prop, C.XA_STRING, val, #val, 8) end local function decode(data, n, bits) assert(bits == 8, 'invalid format') return ffi.string(data, n) end function get_string_prop(win, prop) return get_prop(win, prop, decode) end local function list_encoder(ctype, encode_val) encode_val = encode_val or glue.pass local arr_ctype = ffi.typeof('$[?]', ffi.typeof(ctype)) return function(t) local n = #t local a = ffi.new(arr_ctype, n) for i = 1,n do a[i-1] = encode_val(t[i]) end return a,n end end local function list_decoder(ctype, decode_val) decode_val = decode_val or glue.pass local ptr_ctype = ffi.typeof('$*', ffi.typeof(ctype)) return function(val, len) val = cast(ptr_ctype, val) local t = {} for i=1,len do t[i] = decode_val(val[i-1]) end return t end end local atombuf = ffi.new'Atom[1]' function set_atom_prop(win, prop, val) atombuf[0] = atom(val) set_prop(win, prop, C.XA_ATOM, atombuf) end local atom_list = list_encoder('Atom', atom) function set_atom_map_prop(win, prop, t) local dt = {} for k,v in pairs(t) do if v then dt[#dt+1] = k end end local atoms, n = atom_list(dt) if n == 0 then delete_prop(win, prop) else set_prop(win, prop, C.XA_ATOM, atoms, n) end end local decode = list_decoder('Atom', tonumber) local function atom_map_index(t, name) return rawget(t, atom(name)) and true or false end function get_atom_map_prop(win, prop, key) local list = get_prop(win, prop, decode) if not list then return end --statically index the atoms local t = {} for i, atom in ipairs(list) do t[atom] = true end --dynamically index the atoms names setmetatable(t, {__index = atom_map_index}) --sugar for key lookup if key then return t[key] else return t end end local nbuf = ffi.new'int[1]' function set_cardinal_prop(win, prop, val) nbuf[0] = val set_prop(win, prop, C.XA_CARDINAL, nbuf, 1) end local function decode_window(val, len) if len == 0 then return end return xid(cast('Window*', val)[0]) end function get_window_prop(win, prop) return get_prop(win, prop, decode_window) end local winbuf = ffi.new'Window[1]' function set_window_prop(win, prop, target_win) winbuf[0] = target_win set_prop(win, prop, C.XA_WINDOW, winbuf, 1) end local decode = list_decoder('Window', xid) function get_window_list_prop(win, prop) return get_prop(win, prop, decode) end local decode = list_decoder('long', tonumber) function get_int_list_prop(win, prop) return get_prop(win, prop, decode) end --client message events --------------------------------------------------- local function client_message_event(win, type, format) local e = ffi.new'XEvent' e.type = C.ClientMessage e.xclient.format = format or 32 e.xclient.window = win e.xclient.message_type = atom(type) return e end local function list_event(win, type, datatype, val_func, ...) local e = client_message_event(win, type) for i = 1,5 do local v = select(i, ...) if v then e.xclient.data[datatype][i-1] = val_func(v) end end return e end function int32_list_event(win, type, ...) return list_event(win, type, 'l', glue.pass, ...) end function atom_list_event(win, type, ...) return list_event(win, type, 'l', atom, ...) end function send_message(win, e, propagate, mask) C.XSendEvent(c, win, propagate or false, mask or 0, e) end function send_message_to_root(e, propagate, mask) local mask = mask or bor( C.SubstructureNotifyMask, C.SubstructureRedirectMask) send_message(screen.root, e, propagate, mask) end --window attributes ------------------------------------------------------- local attrs = ffi.new'XWindowAttributes' function get_attrs(win) assert(C.XGetWindowAttributes(c, win, attrs) == 1) return attrs end function set_attrs(win, t) local mask, attrs = attr_buf(t) if mask == 0 then return end --nothing to set --assert( C.XChangeWindowAttributes(c, win, mask, attrs) -- == 0) end --window geometry --------------------------------------------------------- local rbuf = ffi.new'Window[1]' local xbuf = ffi.new'int[1]' local ybuf = ffi.new'int[1]' local wbuf = ffi.new'unsigned int[1]' local hbuf = ffi.new'unsigned int[1]' local bbuf = ffi.new'unsigned int[1]' local dbuf = ffi.new'unsigned int[1]' function get_geometry(win) --returns x, y, w, h, border_width, depth, root assert(C.XGetGeometry(c, win, rbuf, xbuf, ybuf, wbuf, hbuf, bbuf, dbuf) == 1) return xbuf[0], ybuf[0], wbuf[0], hbuf[0], bbuf[0], dbuf[0], xid(rbuf[0]) end local cbuf = ffi.new'XWindowChanges' local masks = { x = C.CWX, y = C.CWY, width = C.CWWidth, height = C.CWHeight, border_width = C.CWBorderWidth, stack_mode = C.CWStackMode, sibling = C.CWSibling, } function config(win, t) local mask = maskedset(cbuf, t, masks) if mask == 0 then return end --nothing to set cbuf.width = math.max(1, cbuf.width) --prevent BadValue errors cbuf.height = math.max(1, cbuf.height) C.XConfigureWindow(c, win, mask, cbuf) end --coordinate space translation -------------------------------------------- local xbuf = ffi.new'int[1]' local ybuf = ffi.new'int[1]' local winbuf = ffi.new'Window[1]' function translate_coords(src_win, dst_win, x, y) if C.XTranslateCoordinates(c, src_win, dst_win, x, y, xbuf, ybuf, winbuf) == 0 then return --windows are on different screens end return xbuf[0], ybuf[0], xid(winbuf[0]) end --_NET_SUPPORTED property ------------------------------------------------- --it's a big atom map which tells which features the server supports. net_supported_map = glue.memoize(function() return get_atom_map_prop(screen.root, '_NET_SUPPORTED') end) function net_supported(s) return net_supported_map()[s] end --_NET_WM_STATE property -------------------------------------------------- --tracks maximization, minimization/hiding (read-only), fullscreen and topmost flags. --NOTE: all _NET_WM_STATE flags are cleared when withdrawing a window! --The position and size of the window in normal state are also lost. function get_net_wm_state(win, key) return get_atom_map_prop(win, '_NET_WM_STATE', key) end function set_net_wm_state(win, t) --before the window is mapped, use this. set_atom_map_prop(win, '_NET_WM_STATE', t) end function change_net_wm_state(win, set, atom1, atom2) --after a window is mapped, use this. local e = atom_list_event(win, '_NET_WM_STATE', set and 1 or 0, atom1, atom2) send_message_to_root(e) end --NOTE: this tracks minimization too, without distinguishing from hiding. function get_net_wm_state_hidden(win) return get_net_wm_state(win, '_NET_WM_STATE_HIDDEN') or false end function get_net_wm_state_maximized(win) return get_net_wm_state(win, '_NET_WM_STATE_MAXIMIZED_HORZ') or false end function change_net_wm_state_maximized(win, maximized) change_net_wm_state(win, maximized, '_NET_WM_STATE_MAXIMIZED_HORZ', '_NET_WM_STATE_MAXIMIZED_VERT') end function get_net_wm_state_fullscreen(win) return get_net_wm_state(win, '_NET_WM_STATE_FULLSCREEN') or false end function change_net_wm_state_fullscreen(win, fullscreen) return change_net_wm_state(win, fullscreen, '_NET_WM_STATE_FULLSCREEN') end function get_net_wm_state_topmost(win) return get_net_wm_state(win, '_NET_WM_STATE_ABOVE') or false end function change_net_wm_state_topmost(win, topmost) change_net_wm_state(win, topmost, '_NET_WM_STATE_ABOVE') end --WM_HINTS property ------------------------------------------------------- --for specifying additional _initial_ parameters (it doesn't track them!). --NOTE: setting initial_state to IconicState results in the clearing of --any _NET_WM_STATE flags and replacing them with _NET_WM_STATE_HIDDEN --which means the maximized and fullscreen states cannot be tracked anymore. --So better leave initial_state alone and set _NET_WM_STATE_HIDDEN instead --if you want to show a window in minimized state _and_ have the maximized --and fullscreen states still available for querying while minimized. local hints = ptr(C.XAllocWMHints(), C.XFree) local masks = { input = C.InputHint, initial_state = C.StateHint, icon_pixmap = C.IconPixmapHint, icon_x = C.IconWindowHint, icon_y = C.IconWindowHint, icon_mask = C.IconMaskHint, window_group = C.WindowGroupHint, } function get_wm_hints(win) local hints = ptr(C.XGetWMHints(c, win)) if not hints then return end local t = maskedget(hints, hints.flags, masks) C.XFree(hints) return t end function set_wm_hints(win, t) hints.flags = maskedset(hints, t, masks) if hints.flags == 0 then return end C.XSetWMHints(c, win, hints) end --WM_NORMAL_HINTS property ------------------------------------------------ --tracks window constraints and sets window's _initial_ position. local hints = ptr(C.XAllocSizeHints(), C.XFree) local masks = { x = C.USPosition, --C.PPosition, y = C.USPosition, --C.PPosition, width = C.USSize, --C.PSize, height = C.USSize, --C.PSize, min_width = C.PMinSize, min_height = C.PMinSize, max_width = C.PMaxSize, max_height = C.PMaxSize, } function set_wm_size_hints(win, t, prop) hints.flags = maskedset(hints, t, masks) if hints.flags == 0 then return end prop = atom(prop or 'WM_NORMAL_HINTS') C.XSetWMSizeHints(c, win, hints, prop) end local mask = ffi.new'long[1]' function get_wm_size_hints(win, prop) prop = atom(prop or 'WM_NORMAL_HINTS') if C.XGetWMSizeHints(c, win, hints, mask, prop) == 0 then return end return maskedget(hints, mask[0], masks) end --_MOTIF_WM_HINTS property ------------------------------------------------ --controls window's appearance and available state-changing operations. local function decode_motif_wm_hints(val, len) return ffi.new('PropMotifWmHints', cast('PropMotifWmHints*', val)[0]) end function get_motif_wm_hints(win) get_prop(win, '_MOTIF_WM_HINTS', decode_motif_wm_hints) end function set_motif_wm_hints(win, hints) set_prop(win, '_MOTIF_WM_HINTS', '_MOTIF_WM_HINTS', hints, C.MOTIF_WM_HINTS_ELEMENTS) end --WM_STATE property ------------------------------------------------------- --can be used to distinguish between visible, minimized and hidden states. local function decode_wm_state(val, len) assert(len >= 2) val = ffi.cast('int32_t*', val) return {val[0], val[1]} --ICCCM_WM_STATE_*, icon_window_id end function get_wm_state(win) local t = get_prop(win, 'WM_STATE', decode_wm_state) if t then return unpack(t) end end --returns true if minimized or normal, false if not yet mapped or withdrawn. function get_wm_state_visible(win) local st = xlib.get_wm_state(win) return st and st ~= C.WithdrawnState or false end --returns true if visible and minimized. function get_wm_state_iconic(win) local st, icon_window_id = xlib.get_wm_state(win) return st == C.IconicState, icon_window_id end --WM_TRANSIENT_FOR property ----------------------------------------------- --transient windows stay on top of their target, can't be minimized --or maximized and don't appear in taskbar. they are good for tool windows. local winbuf = ffi.new'Window[1]' function get_transient_for(win) C.XGetTransientForHint(c, win, winbuf) return xid(winbuf[0]) end function set_transient_for(win, for_win) C.XSetTransientForHint(c, win, for_win) end --_NET_FRAME_EXTENTS property --------------------------------------------- function frame_extents_supported() return net_supported'_NET_REQUEST_FRAME_EXTENTS' end --NOTE: _NET_FRAME_EXTENTS is not set up until the window is mapped, but --it can be requested by calling this function. The property is not set --immediately so you'll have to retry getting it a few times. As usual, --expect this to be poorly supported because there's only so much UI --programming mojo out there in the Linux world and it wasn't wasted here. function request_frame_extents(win) local e = client_message_event(win, atom'_NET_REQUEST_FRAME_EXTENTS') send_message_to_root(e) end local function decode_extents(val) val = cast('long*', val) return { tonumber(val[0]), --left tonumber(val[2]), --top tonumber(val[1]), --right tonumber(val[3]), --bottom } end function get_frame_extents(win) local t = get_prop(win, '_NET_FRAME_EXTENTS', decode_extents) if t then return unpack(t) end end --_NET_ACTIVE_WINDOW property --------------------------------------------- --tracks activation of top-level windows. function get_net_active_window() return get_window_prop(screen.root, '_NET_ACTIVE_WINDOW') end function change_net_active_window(win, focused_win) local e = int32_list_event(win, '_NET_ACTIVE_WINDOW', 1, --message comes from an app 0, --timestamp focused_win or C.None) send_message_to_root(e) end --WM_NAME property -------------------------------------------------------- --tracks window title in the titlebar. function get_wm_name(win) return get_string_prop(win, C.XA_WM_NAME) end function set_wm_name(win, name) set_string_prop(win, C.XA_WM_NAME, name) end --WM_ICON_NAME property --------------------------------------------------- --tracks window's title in the taskbar. function get_wm_icon_name(win) return get_string_prop(win, C.XA_WM_ICON_NAME) end function set_wm_icon_name(win, name) set_string_prop(win, C.XA_WM_ICON_NAME, name) end --input focus ------------------------------------------------------------- --NOTE: for top-level windows use the _NET_ACTIVE_WINDOW property instead. local winbuf = ffi.new'Window[1]' local fstate = ffi.new'int[1]' function get_input_focus() C.XGetInputFocus(c, winbuf, fstate) return xid(winbuf[0]), fstate[0] end function set_input_focus(win, fstate) C.XSetInputFocus(c, win, fstate or C.RevertToNone, C.CurrentTime) end --_NET_WM_PING protocol --------------------------------------------------- --a protocol which allows a WM to ping and kill a non-responding app. --respond to a _NET_WM_PING event function pong(e) local reply = ffi.new('XEvent', ffi.cast('XEvent*', e)[0]) reply.type = C.ClientMessage reply.xclient.window = screen.root send_message_to_root(reply) --pong! end --set _NET_WM_PID and WM_CLIENT_MACHINE as needed by the protocol function set_net_wm_ping_info(win) set_cardinal_prop(win, '_NET_WM_PID', ffi.C.xlib_getpid()) local name local buf = ffi.new'char[256]' if ffi.C.xlib_gethostname(buf, 256) == 0 then name = ffi.string(buf) else local utsname = ffi.new'xlib_utsname' if ffi.C.xlib_uname(utsname) == 0 then name = ffi.string(utsname.nodename) end end if name then set_string_prop(win, 'WM_CLIENT_MACHINE', name) end end --window commands --------------------------------------------------------- --NOTE: XMapWindow doesn't activate minimized windows, but does active hidden windows. --NOTE: MapNotify is not sent if the window was minimized. function map(win) C.XMapWindow(c, win) end --NOTE: XWithdrawWindow should always be used instead of XUnmapWindow --for top-level windows because XUnmapWindow doesn't send the synthetic --UnmapNotify required per ICCCM, so it doesn't properly hide minimized windows. --NOTE: UnmapNotify is not sent if the window was minimized. function withdraw(win) C.XWithdrawWindow(c, win, screen_num) end --NOTE: minimizing implies unmapping. To unminimize, call map(). function iconify(win) C.XIconifyWindow(c, win, screen_num) --sends WM_CHANGE_STATE message end --selections -------------------------------------------------------------- function get_selection_owner(sel) return xid(C.XGetSelectionOwner(c, atom(sel))) end --_NET_WORKAREA property -------------------------------------------------- --tracks desktop area (screen minus taskbars) for the main screen. --for secondary screens you're out of luck (told you about that mojo). function get_net_workarea(screen1, desktop_num) local screen = screen1 or screen local t = get_int_list_prop(screen.root, '_NET_WORKAREA') if not t then return end local dt = {} for i=1,#t,4 do dt[#dt+1] = {unpack(t, i, i+3)} end return desktop_num and dt[desktop_num] or dt end --XSETTINGS extension ----------------------------------------------------- --this is a dictionary for global desktop settings probably born --out of a Unix programmer's love and interest for ad-hoc data formats. function get_xsettings_window(screen0) local snum = C.XScreenNumberOfScreen(screen0 or screen) return get_selection_owner('_XSETTINGS_S'..snum) end function set_xsettings_change_notify() C.XGrabServer(c) local ok, err = pcall(function() local win = get_xsettings_window() if not win then return end C.XSelectInput(c, win, bor(C.StructureNotifyMask, C.PropertyChangeMask)) end) C.XUngrabServer(c) assert(ok, err) end function get_xsettings() local xsettings = require'xlib_xsettings' local win = get_xsettings_window() if not win then return end return get_prop(win, '_XSETTINGS_SETTINGS', xsettings.decode) end --rendering --------------------------------------------------------------- --NOTE: XClearWindow() doesn't generate Expose events. function clear_area(win, x, y, w, h) assert(C.XClearArea(c, win, x, y, w, h, true) == 1) end local image = ffi.new'XImage' function put_image(gc, data, size, w, h, depth, xid, dx, dy) image.width = w image.height = h image.format = C.ZPixmap image.data = data image.bitmap_unit = 8 image.byte_order = C.LSBFirst image.bitmap_bit_order = C.LSBFirst image.depth = depth image.bytes_per_line = w * 4 image.bits_per_pixel = 32 image.red_mask = 0xff image.green_mask = 0xff image.blue_mask = 0xff C.XInitImage(image) C.XPutImage(c, xid, gc, image, 0, 0, dx or 0, dy or 0, w, h) end function copy_area(gc, src, sx, sy, w, h, dst, dx, dy) C.XCopyArea(c, src, dst, gc, sx or 0, sy or 0, w, h, dx or 0, dy or 0) end --Xinerama extension ------------------------------------------------------ --lists virtual screens in multi-monitor setups. local XRC function xinerama_screens() if not has_extension'XINERAMA' then return end XRC = XRC or ffi.load'Xinerama.so.1' if XRC.XineramaIsActive(c) == 0 then return end local nbuf = ffi.new'int[1]' local screens = ptr(XRC.XineramaQueryScreens(c, nbuf), C.XFree) return screens, nbuf[0] end --keyboard ---------------------------------------------------------------- local buf function query_keymap() buf = buf or ffi.new'char[32]' C.XQueryKeymap(c, buf) return buf end --NOTE: XKeycodeToKeysym() is deprecated and can be replaced with --XGetKeyboardMapping() or XkbKeycodeToKeysym(). function keysym(keycode, group, level) return C.XkbKeycodeToKeysym(c, keycode, group, level) end function keycode(keysym) return C.XKeysymToKeycode(c, keysym) end local state function get_keyboard_control() state = state or ffi.new'XKeyboardState' C.XGetKeyboardControl(c, state) return state end --mouse ------------------------------------------------------------------- local rootbuf = ffi.new'Window[1]' local childbuf = ffi.new'Window[1]' local xbuf = ffi.new'int[4]' local maskbuf = ffi.new'unsigned int[1]' function query_pointer(win) --returns x, y, buttons, child_win, root_win if C.XQueryPointer(c, win, rootbuf, childbuf, xbuf, xbuf+1, xbuf+2, xbuf+3, maskbuf) == 1 then return xbuf[2], xbuf[3], maskbuf[0], childbuf[0], rootbuf[0] end end --cursors ----------------------------------------------------------------- ffi.cdef'Cursor XcursorLibraryLoadCursor (Display *dpy, const char *file);' local cursors = { --nw standard arrow = {'left_ptr', 'arrow', 'dnd-none', 'op_left_arrow'}, text = {'ibeam', 'xterm', 'text'}, hand = {'pointing_hand', 'hand2', 'hand', 'hand1', 'pointer', 'e29285e634086352946a0e7090d73106', '9d800788f1b08800ae810202380a0822'}, cross = {'cross', 'diamond-cross', 'cross-reverse', 'crosshair'}, forbidden = {'forbidden', 'circle', 'dnd-no-drop', 'not-allowed', '03b6e0fcb3499374a867c041f52298f0'}, size_diag1 = {'size_bdiag', 'fd_double_arrow', 'bottom_left_corner', 'top_right_corner', 'fcf1c3c7cd4491d801f1e1c78f100000'}, size_diag2 = {'size_fdiag', 'bd_double_arrow', 'bottom_right_corner', 'top_left_corner', 'c7088f0f3e6c8088236ef8e1e3e70000'}, size_v = {'size_ver', 'sb_v_double_arrow', 'v_double_arrow', 'n-resize', 's-resize', 'col-resize', 'top_side', 'bottom_side', 'base_arrow_up', 'base_arrow_down', 'based_arrow_down', 'based_arrow_up', '00008160000006810000408080010102'}, size_h = {'size_hor', 'sb_h_double_arrow', 'h_double_arrow', 'e-resize', 'w-resize', 'row-resize', 'right_side', 'left_side', '028006030e0e7ebffc7f7070c0600140'}, move = {'fleur', 'size_all'}, busy_arrow = {'left_ptr_watch', 'half-busy', '3ecb610c1bf2410f44200f48c40d3599', '00000000000000020006000e7e9ffc3f', '08e8e1c95fe2fc01f976f1e063a24ccd'}, --resizing sides and corners top = {'top_side'}, left = {'left_side'}, bottom = {'bottom_side'}, right = {'right_side'}, topleft = {'top_left_corner'}, bottomright = {'bottom_right_corner'}, bottomleft = {'bottom_left_corner'}, topright = {'top_right_corner'}, --extra up_arrow = {'sb_up_arrow', 'centre_ptr', 'up_arrow', 'center_ptr'}, wait = {'wait', 'watch', 'progress'}, help_arrow = {'whats_this', 'left_ptr_help', 'help', 'question_arrow', 'dnd-ask', 'd9ce0ab605698f320427677b458ad60b', '5c6cd98b3f3ebcb1f9c7f1c204630408'}, split_h = {'split_h', '14fef782d02440884392942c11205230', 'size_hor'}, split_v = {'split_v', '2870a09082c103050810ffdffffe0204', 'size_ver'}, hand_link = {'dnd-link', 'link', 'alias', '3085a0e285430894940527032f8b26df', '640fb0e74195791501fd1ed57b41487f', 'a2a266d0498c3104214a47bd64ab0fc8'}, hand_copy = {'dnd-copy', 'copy', '1081e37283d90000800003c07f3ef6bf', '6407b0e94181790501fd1e167b474872', 'b66166c04f8c3109214a4fbd64a50fc8'}, hand_move = {'dnd-move', 'move'}, } --resizing fallbacks glue.extend(cursors.topleft, cursors.size_diag2) glue.extend(cursors.bottomright, cursors.size_diag2) glue.extend(cursors.topright, cursors.size_diag1) glue.extend(cursors.bottomleft, cursors.size_diag1) glue.extend(cursors.top, cursors.size_v) glue.extend(cursors.bottom, cursors.size_v) glue.extend(cursors.left, cursors.size_h) glue.extend(cursors.right, cursors.size_h) function try_load_cursor(name) local t = cursors[name] if t then for i,name in ipairs(t) do local cur = load_cursor(name) if cur then return cur end end else return load_cursor(name) end end function load_cursor(name) return xid(CC.XcursorLibraryLoadCursor(c, name)) end function set_cursor(win, cursor) set_attrs(win, {cursor = cursor}) end function free_cursor(cursor) C.XFreeCursor(c, cursor) end local bcur function blank_cursor() do return end if not bcur then bcur = gen_id() local pix = gen_id() C.xcb_create_pixmap(c, 1, pix, screen.root, 1, 1) C.xcb_create_cursor(c, bcur, pix, pix, 0, 0, 0, 0, 0, 0, 0, 0) C.xcb_free_pixmap(c, pix) end return bcur end --MIT-SHM extension ------------------------------------------------------- local shm function xlib.shm() if XC.XShmQueryExtension(c) == 0 then return end shm = require'shm' return true end function shm_create_image(visual, depth, w, h) local format = XC.XShmPixmapFormat(c) assert(format == C.ZPixmap) local shminfo = ffi.new'XShmSegmentInfo' local image = ptr(XC.XShmCreateImage(c, visual, depth, format, nil, shminfo, w, h)) assert(image, 'XShmCreateImage failed') shminfo.shmid = shm.shmget( shm.IPC_PRIVATE, image.bytes_per_line * image.height, bit.bor(shm.IPC_CREAT, 0x1ff)) --0777 if shminfo.shmid == 0 then X.XDestroyImage(image) error'shmget failed' end shminfo.shmaddr = shm.shmat(shminfo.shmid, nil, 0) image.data = shminfo.shmaddr if XC.XShmAttach(c, shminfo) == 0 then X.XDestroyImage(image) shm.shmdt(shminfo.shmaddr) shm.shmctl(shminfo.shmid, shm.IPC_RMID, 0) error'XShmAttach failed' end return image, shminfo end function shm_free_image(image, shminfo) XC.XShmDetach(c, shminfo) X.XDestroyImage(image) shm.shmdt(shminfo.shmaddr) shm.shmctl(shminfo.shmid, shm.IPC_RMID, nil) end function shm_put_image(gc, image, w, h, xid, dx, dy) assert(XC.XShmPutImage(c, xid, gc, image, 0, 0, dx or 0, dy or 0, w, h, true) ~= 0) end --[[ --input methods ----------------------------------------------------------- function set_locale_modifiers(im) im = im or 'none' --XIM, SCIM, IBUS, etc. if os.setlocale'' then --set native locale if C.XSupportsLocale() ~= 0 then C.XSetLocaleModifiers('@im='..im) end end end function open_im(win) local im = ptr(C.XOpenIM(c, nil, nil, nil)) if not im then return end local styles = ffi.new'XIMStyles *[1]' failed_arg = ptr(C.XGetIMValues(im, C.XNQueryInputStyle, styles, nil)) if failed_arg then return end for i=0,styles.count_styles-1 do print(string.format('style %d', styles.supported_styles[i]) end local ic = ptr(C.XCreateIC(im, C.XNInputStyle, bor(C.XIMPreeditNothing, C.XIMStatusNothing), C.XNClientWindow, self.win, nil) if not ic then return end C.XSetICFocus(ic) end ]] --------------------------------------------------------------------------- connect(...) return xlib end return M