From 0f7269c4ec666eb8bcf2616abbb5af46087cbdea Mon Sep 17 00:00:00 2001 From: Chris Feng Date: Wed, 6 Feb 2019 00:00:00 +0000 Subject: Add input method support ; The code is basically refactored from ; https://github.com/ch11ng/exim to get better maintenance. * exwm-xim.el: New module making Emacs's builtin input methods usable for interacting with X windows. * exwm-core.el (exwm--intern-atom): New function for intern X11 atoms. * exwm-input.el (exwm-input--init): * exwm-manage.el (exwm-manage--init): Use it. --- exwm-xim.el | 781 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 exwm-xim.el (limited to 'exwm-xim.el') diff --git a/exwm-xim.el b/exwm-xim.el new file mode 100644 index 000000000000..6a213acc0cb5 --- /dev/null +++ b/exwm-xim.el @@ -0,0 +1,781 @@ +;;; exwm-xim.el --- XIM Module for EXWM -*- lexical-binding: t -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; Author: Chris Feng + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; This module adds XIM support for EXWM and allows sending characters +;; generated by any Emacs's builtin input method (info node `Input Methods') +;; to X windows. + +;; This module is essentially an X input method server utilizing Emacs as +;; its backend. It talks with X windows through the XIM protocol. The XIM +;; protocol is quite flexible by itself, stating that an implementation can +;; create network connections of various types as well as make use of an +;; existing X connection for communication, and that an IM server may +;; support multiple transport versions, various input styles and several +;; event flow modals, etc. Here we only make choices that are most popular +;; among other IM servers and more importantly, practical for Emacs to act +;; as an IM server: +;; +;; + Packets are transported on top of an X connection like most IMEs. +;; + Only transport version 0.0 (i.e. only-CM & Property-with-CM) is +;; supported (same as "IM Server Developers Kit", adopted by most IMEs). +;; + Only support static event flow, on-demand-synchronous method. +;; + Only "root-window" input style is supported. + +;; To use this module, first load and enable it as follows: +;; +;; (require 'exwm-xim) +;; (exwm-xim-enable) +;; +;; A keybinding for `toggle-input-method' is probably required to turn on & +;; off an input method (default to `default-input-method'). It's bound to +;; 'C-\' by default and can be made reachable when working with X windows: +;; +;; (push ?\C-\\ exwm-input-prefix-keys) +;; +;; It's also required (and error-prone) to setup environment variables to +;; make applications actually use this input method. Typically the +;; following lines should be inserted into '~/.xinitrc'. +;; +;; export XMODIFIERS=@im=exwm-xim +;; export GTK_IM_MODULE=xim +;; export QT_IM_MODULE=xim +;; export CLUTTER_IM_MODULE=xim + +;; References: +;; + XIM (http://www.x.org/releases/X11R7.6/doc/libX11/specs/XIM/xim.html) +;; + IMdkit (http://xorg.freedesktop.org/archive/unsupported/lib/IMdkit/) +;; + UIM (https://github.com/uim/uim) + +;;; Code: + +(eval-when-compile (require 'cl-lib)) + +(require 'xcb-keysyms) +(require 'xcb-xim) + +(require 'exwm-core) +(require 'exwm-input) + +(defconst exwm-xim--locales + "@locale=\ +aa,af,ak,am,an,anp,ar,as,ast,ayc,az,be,bem,ber,bg,bhb,bho,bn,bo,br,brx,bs,byn,\ +ca,ce,cmn,crh,cs,csb,cv,cy,da,de,doi,dv,dz,el,en,es,et,eu,fa,ff,fi,fil,fo,fr,\ +fur,fy,ga,gd,gez,gl,gu,gv,ha,hak,he,hi,hne,hr,hsb,ht,hu,hy,ia,id,ig,ik,is,it,\ +iu,iw,ja,ka,kk,kl,km,kn,ko,kok,ks,ku,kw,ky,lb,lg,li,li,lij,lo,lt,lv,lzh,mag,\ +mai,mg,mhr,mi,mk,ml,mn,mni,mr,ms,mt,my,nan,nb,nds,ne,nhn,niu,nl,nn,nr,nso,oc,\ +om,or,os,pa,pa,pap,pl,ps,pt,quz,raj,ro,ru,rw,sa,sat,sc,sd,se,shs,si,sid,sk,sl,\ +so,sq,sr,ss,st,sv,sw,szl,ta,tcy,te,tg,th,the,ti,tig,tk,tl,tn,tr,ts,tt,ug,uk,\ +unm,ur,uz,ve,vi,wa,wae,wal,wo,xh,yi,yo,yue,zh,zu,\ +C,no" + "All supported locales (stolen from glibc).") + +(defconst exwm-xim--default-error + (make-instance 'xim:error + :im-id 0 + :ic-id 0 + :flag xim:error-flag:invalid-both + :error-code xim:error-code:bad-something + :length 0 + :type 0 + :detail nil) + "Default error returned to clients.") + +(defconst exwm-xim--default-im-attrs + (list (make-instance 'xim:XIMATTR + :id 0 + :type xim:ATTRIBUTE-VALUE-TYPE:xim-styles + :length (length xlib:XNQueryInputStyle) + :attribute xlib:XNQueryInputStyle)) + "Default IM attrs returned to clients.") + +(defconst exwm-xim--default-ic-attrs + (list (make-instance 'xim:XICATTR + :id 0 + :type xim:ATTRIBUTE-VALUE-TYPE:long-data + :length (length xlib:XNInputStyle) + :attribute xlib:XNInputStyle) + (make-instance 'xim:XICATTR + :id 1 + :type xim:ATTRIBUTE-VALUE-TYPE:window + :length (length xlib:XNClientWindow) + :attribute xlib:XNClientWindow) + ;; Required by e.g. xterm. + (make-instance 'xim:XICATTR + :id 2 + :type xim:ATTRIBUTE-VALUE-TYPE:window + :length (length xlib:XNFocusWindow) + :attribute xlib:XNFocusWindow)) + "Default IC attrs returned to clients.") + +(defconst exwm-xim--default-styles + (make-instance 'xim:XIMStyles + :number nil + :styles (list (logior xlib:XIMPreeditNothing + xlib:XIMStatusNothing))) + "Default styles: root-window, i.e. no preediting or status display support.") + +(defconst exwm-xim--default-attributes + (list (make-instance 'xim:XIMATTRIBUTE + :id 0 + :length nil + :value exwm-xim--default-styles)) + "Default IM/IC attributes returned to clients.") + +(defvar exwm-xim--conn nil + "The X connection for initiating other XIM connections.") +(defvar exwm-xim--event-xwin nil + "X window for initiating new XIM connections.") +(defvar exwm-xim--server-client-plist '(nil nil) + "Plist mapping server window to [X connection, client window, byte-order].") +(defvar exwm-xim--client-server-plist '(nil nil) + "Plist mapping client window to server window.") +(defvar exwm-xim--property-index 0 "For generating a unique property name.") +(defvar exwm-xim--im-id 0 "Last IM ID.") +(defvar exwm-xim--ic-id 0 "Last IC ID.") +(defvar exwm-xim--event-pending nil + "Indicating whether Emacs requires more events.") + +;; X11 atoms. +(defvar exwm-xim--@server nil) +(defvar exwm-xim--LOCALES nil) +(defvar exwm-xim--TRANSPORT nil) +(defvar exwm-xim--XIM_SERVERS nil) +(defvar exwm-xim--_XIM_PROTOCOL nil) +(defvar exwm-xim--_XIM_XCONNECT nil) + +(defun exwm-xim--on-SelectionRequest (data _synthetic) + "Handle SelectionRequest events on IMS window. + +Such events would be received when clients query for LOCALES or TRANSPORT." + (exwm--log) + (let ((evt (make-instance 'xcb:SelectionRequest)) + value fake-event) + (xcb:unmarshal evt data) + (with-slots (time requestor selection target property) evt + (setq value (cond ((= target exwm-xim--LOCALES) + ;; Return supported locales. + exwm-xim--locales) + ((= target exwm-xim--TRANSPORT) + ;; Use XIM over an X connection. + "@transport=X/"))) + (when value + ;; Change the property. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Replace + :window requestor + :property property + :type target + :format 8 + :data-len (length value) + :data value)) + ;; Send a SelectionNotify event. + (setq fake-event (make-instance 'xcb:SelectionNotify + :time time + :requestor requestor + :selection selection + :target target + :property property)) + (xcb:+request exwm-xim--conn + (make-instance 'xcb:SendEvent + :propagate 0 + :destination requestor + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal fake-event exwm-xim--conn))) + (xcb:flush exwm-xim--conn))))) + +(cl-defun exwm-xim--on-ClientMessage-0 (data _synthetic) + "Handle ClientMessage event on IMS window (new connection). + +Such events would be received when clients request for _XIM_XCONNECT. +A new X connection and server window would be created to communicate with +this client." + (exwm--log) + (let ((evt (make-instance 'xcb:ClientMessage)) + conn client-xwin server-xwin) + (xcb:unmarshal evt data) + (with-slots (window type data) evt + (unless (= type exwm-xim--_XIM_XCONNECT) + ;; Only handle _XIM_XCONNECT. + (exwm--log "Ignore ClientMessage %s" type) + (cl-return-from exwm-xim--on-ClientMessage-0)) + (setq client-xwin (elt (slot-value data 'data32) 0) + ;; Create a new X connection and a new server window. + conn (xcb:connect) + server-xwin (xcb:generate-id conn)) + (set-process-query-on-exit-flag (slot-value conn 'process) nil) + ;; Store this client. + (plist-put exwm-xim--server-client-plist server-xwin + `[,conn ,client-xwin nil]) + (plist-put exwm-xim--client-server-plist client-xwin server-xwin) + ;; Select DestroyNotify events on this client window. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeWindowAttributes + :window client-xwin + :value-mask xcb:CW:EventMask + :event-mask xcb:EventMask:StructureNotify)) + (xcb:flush exwm-xim--conn) + ;; Handle ClientMessage events from this new connection. + (xcb:+event conn 'xcb:ClientMessage #'exwm-xim--on-ClientMessage) + ;; Create a communication window. + (xcb:+request conn + (make-instance 'xcb:CreateWindow + :depth 0 + :wid server-xwin + :parent exwm--root + :x 0 + :y 0 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOutput + :visual 0 + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + (xcb:flush conn) + ;; Send connection establishment ClientMessage. + (setf window client-xwin + (slot-value data 'data32) `(,server-xwin 0 0 0 0)) + (slot-makeunbound data 'data8) + (slot-makeunbound data 'data16) + (xcb:+request exwm-xim--conn + (make-instance 'xcb:SendEvent + :propagate 0 + :destination client-xwin + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal evt exwm-xim--conn))) + (xcb:flush exwm-xim--conn)))) + +(cl-defun exwm-xim--on-ClientMessage (data _synthetic) + "Handle ClientMessage event on IMS communication window (request). + +Such events would be received when clients request for _XIM_PROTOCOL. +The actual XIM request is in client message data or a property." + (exwm--log) + (let ((evt (make-instance 'xcb:ClientMessage)) + conn client-xwin server-xwin) + (xcb:unmarshal evt data) + (with-slots (format window type data) evt + (unless (= type exwm-xim--_XIM_PROTOCOL) + (exwm--log "Ignore ClientMessage %s" type) + (cl-return-from exwm-xim--on-ClientMessage)) + (setq server-xwin window + conn (plist-get exwm-xim--server-client-plist server-xwin) + client-xwin (elt conn 1) + conn (elt conn 0)) + (cond ((= format 8) + ;; Data. + (exwm-xim--on-request (vconcat (slot-value data 'data8)) + conn client-xwin server-xwin)) + ((= format 32) + ;; Atom. + (with-slots (data32) data + (with-slots (value) + (xcb:+request-unchecked+reply conn + (make-instance 'xcb:GetProperty + :delete 1 + :window server-xwin + :property (elt data32 1) + :type xcb:GetPropertyType:Any + :long-offset 0 + :long-length (elt data32 0))) + (when (> (length value) 0) + (exwm-xim--on-request value conn client-xwin + server-xwin))))))))) + +(defun exwm-xim--on-request (data conn client-xwin server-xwin) + "Handle an XIM reuqest." + (exwm--log) + (let ((opcode (elt data 0)) + ;; Let-bind `xim:lsb' to make pack/unpack functions work correctly. + (xim:lsb (elt (plist-get exwm-xim--server-client-plist server-xwin) 2)) + req replies) + (cond ((= opcode xim:opcode:error) + (exwm--log "ERROR: %s" data)) + ((= opcode xim:opcode:connect) + (exwm--log "CONNECT") + (setq xim:lsb (= (elt data 4) xim:connect-byte-order:lsb-first)) + ;; Store byte-order. + (setf (elt (plist-get exwm-xim--server-client-plist server-xwin) 2) + xim:lsb) + (setq req (make-instance 'xim:connect)) + (xcb:unmarshal req data) + (if (and (= (slot-value req 'major-version) 1) + (= (slot-value req 'minor-version) 0) + ;; Do not support authentication. + (= (slot-value req 'number) 0)) + ;; Accept the connection. + (push (make-instance 'xim:connect-reply) replies) + ;; Deny it. + (push exwm-xim--default-error replies))) + ((memq opcode (list xim:opcode:auth-required + xim:opcode:auth-reply + xim:opcode:auth-next + xim:opcode:auth-ng)) + (exwm--log "AUTH: %d" opcode) + ;; Deny any attempt to make authentication. + (push exwm-xim--default-error replies)) + ((= opcode xim:opcode:disconnect) + (exwm--log "DISCONNECT") + ;; Gracefully disconnect from the client. + (exwm-xim--make-request (make-instance 'xim:disconnect-reply) + conn client-xwin) + ;; Destroy the communication window & connection. + (xcb:+request conn + (make-instance 'xcb:DestroyWindow + :window server-xwin)) + (xcb:disconnect conn) + ;; Clean up cache. + (cl-remf exwm-xim--server-client-plist server-xwin) + (cl-remf exwm-xim--client-server-plist client-xwin)) + ((= opcode xim:opcode:open) + (exwm--log "OPEN") + ;; Note: We make no check here. + (setq exwm-xim--im-id (if (< exwm-xim--im-id #xffff) + (1+ exwm-xim--im-id) + 1)) + (setq replies + (list + (make-instance 'xim:open-reply + :im-id exwm-xim--im-id + :im-attrs-length nil + :im-attrs exwm-xim--default-im-attrs + :ic-attrs-length nil + :ic-attrs exwm-xim--default-ic-attrs) + (make-instance 'xim:set-event-mask + :im-id exwm-xim--im-id + :ic-id 0 + ;; Static event flow. + :forward-event-mask xcb:EventMask:KeyPress + ;; on-demand-synchronous method. + :synchronous-event-mask + xcb:EventMask:NoEvent)))) + ((= opcode xim:opcode:close) + (exwm--log "CLOSE") + (setq req (make-instance 'xim:close)) + (xcb:unmarshal req data) + (push (make-instance 'xim:close-reply + :im-id (slot-value req 'im-id)) + replies)) + ((= opcode xim:opcode:trigger-notify) + (exwm--log "TRIGGER-NOTIFY") + ;; Only static event flow modal is supported. + (push exwm-xim--default-error replies)) + ((= opcode xim:opcode:encoding-negotiation) + (exwm--log "ENCODING-NEGOTIATION") + (setq req (make-instance 'xim:encoding-negotiation)) + (xcb:unmarshal req data) + (let ((index (cl-position "COMPOUND_TEXT" + (mapcar (lambda (i) (slot-value i 'name)) + (slot-value req 'names)) + :test #'equal))) + (unless index + ;; Fallback to portable character encoding (a subset of ASCII). + (setq index -1)) + (push (make-instance 'xim:encoding-negotiation-reply + :im-id (slot-value req 'im-id) + :category + xim:encoding-negotiation-reply-category:name + :index index) + replies))) + ((= opcode xim:opcode:query-extension) + (exwm--log "QUERY-EXTENSION") + (setq req (make-instance 'xim:query-extension)) + (xcb:unmarshal req data) + (push (make-instance 'xim:query-extension-reply + :im-id (slot-value req 'im-id) + ;; No extension support. + :length 0 + :extensions nil) + replies)) + ((= opcode xim:opcode:set-im-values) + (exwm--log "SET-IM-VALUES") + ;; There's only one possible input method attribute. + (setq req (make-instance 'xim:set-im-values)) + (xcb:unmarshal req data) + (push (make-instance 'xim:set-im-values-reply + :im-id (slot-value req 'im-id)) + replies)) + ((= opcode xim:opcode:get-im-values) + (exwm--log "GET-IM-VALUES") + (setq req (make-instance 'xim:get-im-values)) + (let (im-attributes-id) + (xcb:unmarshal req data) + (setq im-attributes-id (slot-value req 'im-attributes-id)) + (if (cl-notevery (lambda (i) (= i 0)) im-attributes-id) + ;; Only support one IM attributes. + (push (make-instance 'xim:error + :im-id (slot-value req 'im-id) + :ic-id 0 + :flag xim:error-flag:invalid-ic-id + :error-code xim:error-code:bad-something + :length 0 + :type 0 + :detail nil) + replies) + (push + (make-instance 'xim:get-im-values-reply + :im-id (slot-value req 'im-id) + :length nil + :im-attributes exwm-xim--default-attributes) + replies)))) + ((= opcode xim:opcode:create-ic) + (exwm--log "CREATE-IC") + (setq req (make-instance 'xim:create-ic)) + (xcb:unmarshal req data) + ;; Note: The ic-attributes slot is ignored. + (setq exwm-xim--ic-id (if (< exwm-xim--ic-id #xffff) + (1+ exwm-xim--ic-id) + 1)) + (push (make-instance 'xim:create-ic-reply + :im-id (slot-value req 'im-id) + :ic-id exwm-xim--ic-id) + replies)) + ((= opcode xim:opcode:destroy-ic) + (exwm--log "DESTROY-IC") + (setq req (make-instance 'xim:destroy-ic)) + (xcb:unmarshal req data) + (push (make-instance 'xim:destroy-ic-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id)) + replies)) + ((= opcode xim:opcode:set-ic-values) + (exwm--log "SET-IC-VALUES") + (setq req (make-instance 'xim:set-ic-values)) + (xcb:unmarshal req data) + ;; We don't distinguish between input contexts. + (push (make-instance 'xim:set-ic-values-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id)) + replies)) + ((= opcode xim:opcode:get-ic-values) + (exwm--log "GET-IC-VALUES") + (setq req (make-instance 'xim:get-ic-values)) + (xcb:unmarshal req data) + (push (make-instance 'xim:get-ic-values-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id) + :length nil + :ic-attributes exwm-xim--default-attributes) + replies)) + ((= opcode xim:opcode:set-ic-focus) + (exwm--log "SET-IC-FOCUS") + ;; All input contexts are the same. + ) + ((= opcode xim:opcode:unset-ic-focus) + (exwm--log "UNSET-IC-FOCUS") + ;; All input contexts are the same. + ) + ((= opcode xim:opcode:forward-event) + (exwm--log "FORWARD-EVENT") + (setq req (make-instance 'xim:forward-event)) + (xcb:unmarshal req data) + (let ((im-func (with-current-buffer (window-buffer) + input-method-function)) + key-event keysym event result) + ;; Note: The flag slot is ignored. + ;; Do conversion in client's byte-order. + (let ((xcb:lsb xim:lsb)) + (setq key-event (make-instance 'xcb:KeyPress)) + (xcb:unmarshal key-event (slot-value req 'event))) + (with-slots (detail state) key-event + (setq keysym (xcb:keysyms:keycode->keysym exwm-xim--conn detail + state)) + (when (/= (car keysym) 0) + (setq event (xcb:keysyms:keysym->event + exwm-xim--conn + (car keysym) + (logand state (lognot (cdr keysym))))))) + (if exwm-xim--event-pending + ;; In case any event reaches here, it should be forwarded + ;; to Emacs. + (when event + (setq unread-command-events + (append unread-command-events (list event)))) + (setq exwm-xim--event-pending t) + (if (or (not im-func) + ;; `list' is the default method. + (eq im-func #'list) + (not event) + ;; Select only printable keys. + (not (integerp event)) (> #x20 event) (< #x7e event)) + ;; Either there is no active input method, or invalid key + ;; is detected. + (with-slots (im-id ic-id serial-number event) req + (push (make-instance 'xim:forward-event + :im-id im-id + :ic-id ic-id + :flag xim:commit-flag:synchronous + :serial-number serial-number + :event event) + replies)) + (when (eq exwm--selected-input-mode 'char-mode) + ;; Grab keyboard temporarily for char-mode. + (exwm-input--grab-keyboard)) + (unwind-protect + (with-temp-buffer + ;; Always show key strokes. + (let ((input-method-use-echo-area t)) + (setq result (funcall im-func event)))) + (when (eq exwm--selected-input-mode 'char-mode) + (exwm-input--release-keyboard))) + ;; This also works for portable character encoding. + (setq result + (encode-coding-string (concat result) + 'compound-text-with-extensions)) + (message "") + (push + (make-instance 'xim:commit-x-lookup-chars + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id) + :flag (logior xim:commit-flag:synchronous + xim:commit-flag:x-lookup-chars) + :length (length result) + :string result) + replies)) + (setq exwm-xim--event-pending nil)))) + ((= opcode xim:opcode:sync) + (exwm--log "SYNC") + (setq req (make-instance 'xim:sync)) + (xcb:unmarshal req data) + (push (make-instance 'xim:sync-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id)) + replies)) + ((= opcode xim:opcode:sync-reply) + (exwm--log "SYNC-REPLY")) + ((= opcode xim:opcode:reset-ic) + (exwm--log "RESET-IC") + ;; No context-specific data saved. + (setq req (make-instance 'xim:reset-ic)) + (xcb:unmarshal req data) + (push (make-instance 'xim:reset-ic-reply + :im-id (slot-value req 'im-id) + :ic-id (slot-value req 'ic-id) + :length 0 + :string "") + replies)) + ((memq opcode (list xim:opcode:str-conversion-reply + xim:opcode:preedit-start-reply + xim:opcode:preedit-caret-reply)) + (exwm--log "PREEDIT: %d" opcode) + ;; No preedit support. + (push exwm-xim--default-error replies)) + (t + (exwm--log "Bad protocol") + (push exwm-xim--default-error replies))) + ;; Actually send the replies. + (when replies + (mapc (lambda (reply) + (exwm-xim--make-request reply conn client-xwin)) + replies) + (xcb:flush conn)))) + +(defun exwm-xim--make-request (req conn client-xwin) + "Make an XIM request REQ via connection CONN. + +CLIENT-XWIN would receive a ClientMessage event either telling the client +the request data or where to fetch the data." + (exwm--log) + (let ((data (xcb:marshal req)) + property format client-message-data client-message) + (if (<= (length data) 20) + ;; Send short requests directly with client messages. + (setq format 8 + ;; Pad to 20 bytes. + data (append data (make-list (- 20 (length data)) 0)) + client-message-data (make-instance 'xcb:ClientMessageData + :data8 data)) + ;; Send long requests with properties. + (setq property (exwm--intern-atom (format "_EXWM_XIM_%x" + exwm-xim--property-index))) + (cl-incf exwm-xim--property-index) + (xcb:+request conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Append + :window client-xwin + :property property + :type xcb:Atom:STRING + :format 8 + :data-len (length data) + :data data)) + ;; Also send a client message to notify the client about this property. + (setq format 32 + client-message-data (make-instance 'xcb:ClientMessageData + :data32 `(,(length data) + ,property + ;; Pad to 20 bytes. + 0 0 0)))) + ;; Send the client message. + (setq client-message (make-instance 'xcb:ClientMessage + :format format + :window client-xwin + :type exwm-xim--_XIM_PROTOCOL + :data client-message-data)) + (xcb:+request conn + (make-instance 'xcb:SendEvent + :propagate 0 + :destination client-xwin + :event-mask xcb:EventMask:NoEvent + :event (xcb:marshal client-message conn))))) + +(defun exwm-xim--on-DestroyNotify (data synthetic) + "Do cleanups on receiving DestroyNotify event. + +Such event would be received when the client window is destroyed." + (exwm--log) + (unless synthetic + (let ((evt (make-instance 'xcb:DestroyNotify)) + conn client-xwin server-xwin) + (xcb:unmarshal evt data) + (setq client-xwin (slot-value evt 'window) + server-xwin (plist-get exwm-xim--client-server-plist client-xwin)) + (when server-xwin + (setq conn (aref (plist-get exwm-xim--server-client-plist server-xwin) + 0)) + (cl-remf exwm-xim--server-client-plist server-xwin) + (cl-remf exwm-xim--client-server-plist client-xwin) + ;; Destroy the communication window & connection. + (xcb:+request conn + (make-instance 'xcb:DestroyWindow + :window server-xwin)) + (xcb:disconnect conn))))) + +(cl-defun exwm-xim--init () + "Initialize the XIM module." + (exwm--log) + (when exwm-xim--conn + (cl-return-from exwm-xim--init)) + ;; Initialize atoms. + (setq exwm-xim--@server (exwm--intern-atom "@server=exwm-xim") + exwm-xim--LOCALES (exwm--intern-atom "LOCALES") + exwm-xim--TRANSPORT (exwm--intern-atom "TRANSPORT") + exwm-xim--XIM_SERVERS (exwm--intern-atom "XIM_SERVERS") + exwm-xim--_XIM_PROTOCOL (exwm--intern-atom "_XIM_PROTOCOL") + exwm-xim--_XIM_XCONNECT (exwm--intern-atom "_XIM_XCONNECT")) + ;; Create a new connection and event window. + (setq exwm-xim--conn (xcb:connect) + exwm-xim--event-xwin (xcb:generate-id exwm-xim--conn)) + (set-process-query-on-exit-flag (slot-value exwm-xim--conn 'process) nil) + ;; Initialize xcb:keysyms module. + (xcb:keysyms:init exwm-xim--conn) + ;; Listen to SelectionRequest event for connection establishment. + (xcb:+event exwm-xim--conn 'xcb:SelectionRequest + #'exwm-xim--on-SelectionRequest) + ;; Listen to ClientMessage event on IMS window for new XIM connection. + (xcb:+event exwm-xim--conn 'xcb:ClientMessage #'exwm-xim--on-ClientMessage-0) + ;; Listen to DestroyNotify event to do cleanups. + (xcb:+event exwm-xim--conn 'xcb:DestroyNotify #'exwm-xim--on-DestroyNotify) + ;; Create the event window. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:CreateWindow + :depth 0 + :wid exwm-xim--event-xwin + :parent exwm--root + :x 0 + :y 0 + :width 1 + :height 1 + :border-width 0 + :class xcb:WindowClass:InputOutput + :visual 0 + :value-mask xcb:CW:OverrideRedirect + :override-redirect 1)) + ;; Set the selection owner. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:SetSelectionOwner + :owner exwm-xim--event-xwin + :selection exwm-xim--@server + :time xcb:Time:CurrentTime)) + ;; Set XIM_SERVERS property on the root window. + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Prepend + :window exwm--root + :property exwm-xim--XIM_SERVERS + :type xcb:Atom:ATOM + :format 32 + :data-len 1 + :data (funcall (if xcb:lsb + #'xcb:-pack-u4-lsb + #'xcb:-pack-u4) + exwm-xim--@server))) + (xcb:flush exwm-xim--conn)) + +(cl-defun exwm-xim--exit () + "Exit the XIM module." + (exwm--log) + ;; Close IMS communication connections. + (mapc (lambda (i) + (when (vectorp i) + (xcb:disconnect (elt i 0)))) + exwm-xim--server-client-plist) + ;; Close the IMS connection. + (unless exwm-xim--conn + (cl-return-from exwm-xim--exit)) + ;; Remove exwm-xim from XIM_SERVERS. + (let ((reply (xcb:+request-unchecked+reply exwm-xim--conn + (make-instance 'xcb:GetProperty + :delete 1 + :window exwm--root + :property exwm-xim--XIM_SERVERS + :type xcb:Atom:ATOM + :long-offset 0 + :long-length 1000))) + unpacked-reply pack unpack) + (unless reply + (cl-return-from exwm-xim--exit)) + (setq reply (slot-value reply 'value)) + (unless (> (length reply) 4) + (cl-return-from exwm-xim--exit)) + (setq reply (vconcat reply) + pack (if xcb:lsb #'xcb:-pack-u4-lsb #'xcb:-pack-u4) + unpack (if xcb:lsb #'xcb:-unpack-u4-lsb #'xcb:-unpack-u4)) + (dotimes (i (/ (length reply) 4)) + (push (funcall unpack reply (* i 4)) unpacked-reply)) + (setq unpacked-reply (delq exwm-xim--@server unpacked-reply) + reply (mapcar pack unpacked-reply)) + (xcb:+request exwm-xim--conn + (make-instance 'xcb:ChangeProperty + :mode xcb:PropMode:Replace + :window exwm--root + :property exwm-xim--XIM_SERVERS + :type xcb:Atom:ATOM + :format 32 + :data-len (length reply) + :data reply)) + (xcb:flush exwm-xim--conn)) + (xcb:disconnect exwm-xim--conn) + (setq exwm-xim--conn nil)) + +(defun exwm-xim-enable () + "Enable XIM support for EXWM." + (exwm--log) + (add-hook 'exwm-init-hook #'exwm-xim--init) + (add-hook 'exwm-exit-hook #'exwm-xim--exit)) + + + +(provide 'exwm-xim) + +;;; exwm-xim.el ends here -- cgit 1.4.1