1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
use std::{
    env,
    ffi::{CStr, CString, IntoStringError},
    fmt,
    os::raw::{c_char, c_ulong, c_ushort},
    ptr,
    sync::{Arc, Mutex},
};

use once_cell::sync::Lazy;

use super::{ffi, util, XConnection, XError};

static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(Default::default);

unsafe fn open_im(xconn: &Arc<XConnection>, locale_modifiers: &CStr) -> Option<ffi::XIM> {
    let _lock = GLOBAL_LOCK.lock();

    // XSetLocaleModifiers returns...
    // * The current locale modifiers if it's given a NULL pointer.
    // * The new locale modifiers if we succeeded in setting them.
    // * NULL if the locale modifiers string is malformed or if the
    //   current locale is not supported by Xlib.
    (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr());

    let im = (xconn.xlib.XOpenIM)(
        xconn.display,
        ptr::null_mut(),
        ptr::null_mut(),
        ptr::null_mut(),
    );

    if im.is_null() {
        None
    } else {
        Some(im)
    }
}

#[derive(Debug)]
pub struct InputMethod {
    pub im: ffi::XIM,
    pub preedit_style: Style,
    pub none_style: Style,
    _name: String,
}

impl InputMethod {
    fn new(xconn: &Arc<XConnection>, im: ffi::XIM, name: String) -> Option<Self> {
        let mut styles: *mut XIMStyles = std::ptr::null_mut();

        // Query the styles supported by the XIM.
        unsafe {
            if !(xconn.xlib.XGetIMValues)(
                im,
                ffi::XNQueryInputStyle_0.as_ptr() as *const _,
                (&mut styles) as *mut _,
                std::ptr::null_mut::<()>(),
            )
            .is_null()
            {
                return None;
            }
        }

        let mut preedit_style = None;
        let mut none_style = None;

        unsafe {
            std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _)
                .iter()
                .for_each(|style| match *style {
                    XIM_PREEDIT_STYLE => {
                        preedit_style = Some(Style::Preedit(*style));
                    }
                    XIM_NOTHING_STYLE if preedit_style.is_none() => {
                        preedit_style = Some(Style::Nothing(*style))
                    }
                    XIM_NONE_STYLE => none_style = Some(Style::None(*style)),
                    _ => (),
                });

            (xconn.xlib.XFree)(styles.cast());
        };

        if preedit_style.is_none() && none_style.is_none() {
            return None;
        }

        let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap());
        let none_style = none_style.unwrap_or(preedit_style);

        Some(InputMethod {
            im,
            _name: name,
            preedit_style,
            none_style,
        })
    }
}

const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle;
const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle;
const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle;

/// Style of the IME context.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Style {
    /// Preedit callbacks.
    Preedit(XIMStyle),

    /// Nothing.
    Nothing(XIMStyle),

    /// No IME.
    None(XIMStyle),
}

impl Default for Style {
    fn default() -> Self {
        Style::None(XIM_NONE_STYLE)
    }
}

#[repr(C)]
#[derive(Debug)]
struct XIMStyles {
    count_styles: c_ushort,
    supported_styles: *const XIMStyle,
}

pub(crate) type XIMStyle = c_ulong;

#[derive(Debug)]
pub enum InputMethodResult {
    /// Input method used locale modifier from `XMODIFIERS` environment variable.
    XModifiers(InputMethod),
    /// Input method used internal fallback locale modifier.
    Fallback(InputMethod),
    /// Input method could not be opened using any locale modifier tried.
    Failure,
}

impl InputMethodResult {
    pub fn is_fallback(&self) -> bool {
        matches!(self, InputMethodResult::Fallback(_))
    }

    pub fn ok(self) -> Option<InputMethod> {
        use self::InputMethodResult::*;
        match self {
            XModifiers(im) | Fallback(im) => Some(im),
            Failure => None,
        }
    }
}

#[derive(Debug, Clone)]
enum GetXimServersError {
    XError(XError),
    GetPropertyError(util::GetPropertyError),
    InvalidUtf8(IntoStringError),
}

// The root window has a property named XIM_SERVERS, which contains a list of atoms represeting
// the availabile XIM servers. For instance, if you're using ibus, it would contain an atom named
// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably
// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale
// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set
// XMODIFIERS to `@server=ibus`?!?"
unsafe fn get_xim_servers(xconn: &Arc<XConnection>) -> Result<Vec<String>, GetXimServersError> {
    let servers_atom = xconn.get_atom_unchecked(b"XIM_SERVERS\0");

    let root = (xconn.xlib.XDefaultRootWindow)(xconn.display);

    let mut atoms: Vec<ffi::Atom> = xconn
        .get_property(root, servers_atom, ffi::XA_ATOM)
        .map_err(GetXimServersError::GetPropertyError)?;

    let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len());
    (xconn.xlib.XGetAtomNames)(
        xconn.display,
        atoms.as_mut_ptr(),
        atoms.len() as _,
        names.as_mut_ptr() as _,
    );
    names.set_len(atoms.len());

    let mut formatted_names = Vec::with_capacity(names.len());
    for name in names {
        let string = CStr::from_ptr(name)
            .to_owned()
            .into_string()
            .map_err(GetXimServersError::InvalidUtf8)?;
        (xconn.xlib.XFree)(name as _);
        formatted_names.push(string.replace("@server=", "@im="));
    }
    xconn.check_errors().map_err(GetXimServersError::XError)?;
    Ok(formatted_names)
}

#[derive(Clone)]
struct InputMethodName {
    c_string: CString,
    string: String,
}

impl InputMethodName {
    pub fn from_string(string: String) -> Self {
        let c_string = CString::new(string.clone())
            .expect("String used to construct CString contained null byte");
        InputMethodName { c_string, string }
    }

    pub fn from_str(string: &str) -> Self {
        let c_string =
            CString::new(string).expect("String used to construct CString contained null byte");
        InputMethodName {
            c_string,
            string: string.to_owned(),
        }
    }
}

impl fmt::Debug for InputMethodName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.string.fmt(f)
    }
}

#[derive(Debug, Clone)]
struct PotentialInputMethod {
    name: InputMethodName,
    successful: Option<bool>,
}

impl PotentialInputMethod {
    pub fn from_string(string: String) -> Self {
        PotentialInputMethod {
            name: InputMethodName::from_string(string),
            successful: None,
        }
    }

    pub fn from_str(string: &str) -> Self {
        PotentialInputMethod {
            name: InputMethodName::from_str(string),
            successful: None,
        }
    }

    pub fn reset(&mut self) {
        self.successful = None;
    }

    pub fn open_im(&mut self, xconn: &Arc<XConnection>) -> Option<InputMethod> {
        let im = unsafe { open_im(xconn, &self.name.c_string) };
        self.successful = Some(im.is_some());
        im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone()))
    }
}

// By logging this struct, you get a sequential listing of every locale modifier tried, where it
// came from, and if it succeeded.
#[derive(Debug, Clone)]
pub(crate) struct PotentialInputMethods {
    // On correctly configured systems, the XMODIFIERS environment variable tells us everything we
    // need to know.
    xmodifiers: Option<PotentialInputMethod>,
    // We have some standard options at our disposal that should ostensibly always work. For users
    // who only need compose sequences, this ensures that the program launches without a hitch
    // For users who need more sophisticated IME features, this is more or less a silent failure.
    // Logging features should be added in the future to allow both audiences to be effectively
    // served.
    fallbacks: [PotentialInputMethod; 2],
    // For diagnostic purposes, we include the list of XIM servers that the server reports as
    // being available.
    _xim_servers: Result<Vec<String>, GetXimServersError>,
}

impl PotentialInputMethods {
    pub fn new(xconn: &Arc<XConnection>) -> Self {
        let xmodifiers = env::var("XMODIFIERS")
            .ok()
            .map(PotentialInputMethod::from_string);
        PotentialInputMethods {
            // Since passing "" to XSetLocaleModifiers results in it defaulting to the value of
            // XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply
            // running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is
            // defined in the profile (or parent environment) then that parent XMODIFIERS is used.
            // If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then
            // XSetLocaleModifiers uses the default local input method. Note that defining
            // XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in
            // that case, we get `None` and end up skipping ahead to the next method.
            xmodifiers,
            fallbacks: [
                // This is a standard input method that supports compose sequences, which should
                // always be available. `@im=none` appears to mean the same thing.
                PotentialInputMethod::from_str("@im=local"),
                // This explicitly specifies to use the implementation-dependent default, though
                // that seems to be equivalent to just using the local input method.
                PotentialInputMethod::from_str("@im="),
            ],
            // The XIM_SERVERS property can have surprising values. For instance, when I exited
            // ibus to run fcitx, it retained the value denoting ibus. Even more surprising is
            // that the fcitx input method could only be successfully opened using "@im=ibus".
            // Presumably due to this quirk, it's actually possible to alternate between ibus and
            // fcitx in a running application.
            _xim_servers: unsafe { get_xim_servers(xconn) },
        }
    }

    // This resets the `successful` field of every potential input method, ensuring we have
    // accurate information when this struct is re-used by the destruction/instantiation callbacks.
    fn reset(&mut self) {
        if let Some(ref mut input_method) = self.xmodifiers {
            input_method.reset();
        }

        for input_method in &mut self.fallbacks {
            input_method.reset();
        }
    }

    pub fn open_im(
        &mut self,
        xconn: &Arc<XConnection>,
        callback: Option<&dyn Fn()>,
    ) -> InputMethodResult {
        use self::InputMethodResult::*;

        self.reset();

        if let Some(ref mut input_method) = self.xmodifiers {
            let im = input_method.open_im(xconn);
            if let Some(im) = im {
                return XModifiers(im);
            } else if let Some(ref callback) = callback {
                callback();
            }
        }

        for input_method in &mut self.fallbacks {
            let im = input_method.open_im(xconn);
            if let Some(im) = im {
                return Fallback(im);
            }
        }

        Failure
    }
}