;;; exwm-xim.el --- XIM Module for EXWM  -*- lexical-binding: t -*-

;; Copyright (C) 2019-2026 Free Software Foundation, Inc.

;; Author: Chris Feng <chris.w.feng@gmail.com>

;; 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 <https://www.gnu.org/licenses/>.

;;; 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, enable it as follows:
;;
;;    (exwm-xim-mode 1)
;;
;; 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
;;
;; However, to avoid deadlocks, emacs.useXIM must be set to false or
;; off in your Xresources (see xrdb(1) and emacs(1)).  Alternatively,
;; you may pass "--xrm=emacs.useXIM:false" as a command-line flag to
;; Emacs.

;; References:
;; + XIM (https://www.x.org/releases/X11R7.6/doc/libX11/specs/XIM/xim.html)
;; + IMdkit (https://xorg.freedesktop.org/archive/unsupported/lib/IMdkit/)
;; + UIM (https://github.com/uim/uim)

;;; Code:

(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.")

;; 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)

(defvar exwm-xim-buffer-p nil
  "Whether current buffer is used by exwm-xim.")
(make-variable-buffer-local 'exwm-xim-buffer-p)

(defun exwm-xim--on-SelectionRequest (data _synthetic)
  "Handle SelectionRequest events on IMS window.
DATA contains unmarshalled SelectionRequest event data.

Such events would be received when clients query for LOCALES or TRANSPORT."
  (exwm--log)
  (with-slots (time requestor selection target property)
      (xcb:unmarshal-new 'xcb:SelectionRequest data)
    (when-let* ((value (cond ((= target exwm-xim--LOCALES)
                              ;; Return supported locales.
                              exwm-xim--locales)
                             ((= target exwm-xim--TRANSPORT)
                              ;; Use XIM over an X connection.
                              "@transport=X/"))))
      ;; 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.
      (let ((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 (xcb:unmarshal-new 'xcb:ClientMessage data))
         (type (slot-value evt 'type))
         (data (slot-value evt 'data)))
    (unless (= type exwm-xim--_XIM_XCONNECT)
      ;; Only handle _XIM_XCONNECT.
      (exwm--log "Ignore ClientMessage %s" type)
      (cl-return-from exwm-xim--on-ClientMessage-0))
    (let* ((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 (slot-value evt '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 DATA 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)
  (with-slots (format window type data)
      (xcb:unmarshal-new 'xcb:ClientMessage data)
    (unless (= type exwm-xim--_XIM_PROTOCOL)
      (exwm--log "Ignore ClientMessage %s" type)
      (cl-return-from exwm-xim--on-ClientMessage))
    (let* ((server-xwin window)
           (client-conn (plist-get exwm-xim--server-client-plist server-xwin))
           (client-xwin (elt client-conn 1))
           (conn (elt client-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))
        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)
           (with-slots (major-version minor-version number)
               (xcb:unmarshal-new 'xim:connect data)
             (if (and (= major-version 1)
                      (= minor-version 0)
                      ;; Do not support authentication.
                      (= 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")
           (with-slots (im-id) (xcb:unmarshal-new 'xim:close data)
             (push (make-instance 'xim:close-reply :im-id 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")
           (with-slots (names im-id)
               (xcb:unmarshal-new 'xim:encoding-negotiation data)
             (let ((index (cl-position "COMPOUND_TEXT"
                                       (mapcar (lambda (i) (slot-value i 'name))
                                               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 im-id
                                    :category
                                    xim:encoding-negotiation-reply-category:name
                                    :index index)
                     replies))))
          ((= opcode xim:opcode:query-extension)
           (exwm--log "QUERY-EXTENSION")
           (with-slots (im-id)
               (xcb:unmarshal-new 'xim:query-extension data)
             (push (make-instance 'xim:query-extension-reply
                                  :im-id 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.
           (with-slots (im-id)
               (xcb:unmarshal-new 'xim:set-im-values data)
             (push (make-instance 'xim:set-im-values-reply
                                  :im-id im-id)
                   replies)))
          ((= opcode xim:opcode:get-im-values)
           (exwm--log "GET-IM-VALUES")
           (with-slots (im-attributes-id im-id)
               (xcb:unmarshal-new 'xim:get-im-values data)
             (if (cl-notevery (lambda (i) (= i 0)) im-attributes-id)
                 ;; Only support one IM attributes.
                 (push (make-instance 'xim:error
                                      :im-id 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 im-id
                               :length nil
                               :im-attributes exwm-xim--default-attributes)
                replies))))
          ((= opcode xim:opcode:create-ic)
           (exwm--log "CREATE-IC")
           (with-slots (im-id)
               (xcb:unmarshal-new 'xim:create-ic 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 im-id
                                  :ic-id exwm-xim--ic-id)
                   replies)))
          ((= opcode xim:opcode:destroy-ic)
           (exwm--log "DESTROY-IC")
           (with-slots (im-id ic-id)
               (xcb:unmarshal-new 'xim:destroy-ic data)
             (push (make-instance 'xim:destroy-ic-reply
                                  :im-id im-id
                                  :ic-id ic-id)
                   replies)))
          ((= opcode xim:opcode:set-ic-values)
           (exwm--log "SET-IC-VALUES")
           (with-slots (im-id ic-id)
               (xcb:unmarshal-new 'xim:set-ic-values data)
             ;; We don't distinguish between input contexts.
             (push (make-instance 'xim:set-ic-values-reply
                                  :im-id im-id
                                  :ic-id ic-id)
                   replies)))
          ((= opcode xim:opcode:get-ic-values)
           (exwm--log "GET-IC-VALUES")
           (with-slots (im-id ic-id)
               (xcb:unmarshal-new 'xim:get-ic-values data)
             (push (make-instance 'xim:get-ic-values-reply
                                  :im-id im-id
                                  :ic-id 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")
           (exwm-xim--handle-forward-event-request
            (xcb:unmarshal-new 'xim:forward-event data)
            xim:lsb conn client-xwin))
          ((= opcode xim:opcode:sync)
           (exwm--log "SYNC")
           (with-slots (im-id ic-id)
               (xcb:unmarshal-new 'xim:sync data)
             (push (make-instance 'xim:sync-reply
                                  :im-id im-id
                                  :ic-id 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.
           (with-slots (im-id ic-id)
               (xcb:unmarshal-new 'xim:reset-ic data)
             (push (make-instance 'xim:reset-ic-reply
                                  :im-id im-id
                                  :ic-id 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--handle-forward-event-request (req lsb conn client-xwin)
  (let ((im-func (with-current-buffer (window-buffer)
                   input-method-function))
        key-event keysym keysyms event result)
    ;; Note: The flag slot is ignored.
    ;; Do conversion in client's byte-order.
    (let ((xcb:lsb lsb))
      (setq key-event (xcb:unmarshal-new 'xcb:KeyPress (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)))))))
    (while (or (slot-value req 'event) unread-command-events)
      (unless (slot-value req 'event)
        (setq event (pop unread-command-events))
        ;; Handle events in (t . EVENT) format.
        (when (and (consp event)
                   (eq (car event) t))
          (setq event (cdr event))))
      (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 ((raw-event event)
                       im-id ic-id serial-number)
              req
            (if raw-event
                (setq event raw-event)
              (setq keysyms (xcb:keysyms:event->keysyms exwm-xim--conn event))
              (with-slots (detail state) key-event
                (setf detail (xcb:keysyms:keysym->keycode exwm-xim--conn
                                                          (caar keysyms))
                      state (cdar keysyms)))
              (setq event (let ((xcb:lsb lsb))
                            (xcb:marshal key-event conn))))
            (when event
              (exwm-xim--make-request
               (make-instance 'xim:forward-event
                              :im-id im-id
                              :ic-id ic-id
                              :flag xim:commit-flag:synchronous
                              :serial-number serial-number
                              :event event)
               conn client-xwin)))
        (when (eq exwm--selected-input-mode 'char-mode)
          ;; Grab keyboard temporarily for char-mode.
          (exwm-input--grab-keyboard))
        (unwind-protect
            (with-temp-buffer
              ;; This variable is used to test whether exwm-xim is enabled.
              ;; Used by e.g. pyim-probe.
              (setq-local exwm-xim-buffer-p t)
              ;; Always show key strokes.
              (let ((input-method-use-echo-area t)
                    (exwm-input-line-mode-passthrough t))
                (setq result (funcall im-func event))
                ;; Clear echo area for the input method.
                (message nil)
                ;; This also works for portable character encoding.
                (setq result
                      (encode-coding-string (concat result)
                                            'compound-text-with-extensions))
                (exwm-xim--make-request
                 (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)
                 conn client-xwin)))
          (when (eq exwm--selected-input-mode 'char-mode)
            (exwm-input--release-keyboard))))
      (xcb:flush conn)
      (setf event nil
            (slot-value req 'event) nil))))

(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)))
      (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
    (when-let* ((evt (xcb:unmarshal-new 'xcb:DestroyNotify data))
                (client-xwin (slot-value evt 'window))
                (server-xwin (plist-get exwm-xim--client-server-plist
                                        client-xwin))
                (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))))

(defun exwm-xim--deadlock-warning ()
  "Warn when XIM may cause Emacs to deadlock."
  ;; NOTE: Checking `system-configuration-features' is unsupported,
  ;; but this is JUST a warning, so it's probably fine.
  (when (and (string-match-p "\\bXIM\\b" system-configuration-features)
             (not (member (x-get-resource "useXIM" "UseXIM")
                          '("false" "off"))))
      (warn "Emacs itself is configured to use XIM and may deadlock \
when trying to communicate with EXWM.  If this happens, set the \
emacs.useXim Xresource to false or off (see xrdb(1) and emacs(1)).")))

(cl-defun exwm-xim--init ()
  "Initialize the XIM module."
  (exwm--log)
  (when exwm-xim--conn
    (cl-return-from exwm-xim--init))
  (exwm-xim--deadlock-warning)
  ;; 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)
            (when (slot-value (elt i 0) 'connected)
              (xcb:disconnect (elt i 0)))))
        exwm-xim--server-client-plist)
  ;; Close the IMS connection.
  (unless (and exwm-xim--conn
               (slot-value exwm-xim--conn 'connected))
    (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))

;;;###autoload
(define-minor-mode exwm-xim-mode
  "Toggle EXWM XIM support."
  :global t
  :group 'exwm
  (exwm--global-minor-mode-body xim))

(provide 'exwm-xim)
;;; exwm-xim.el ends here
