1use std::{
2 env,
3 ffi::{CStr, CString, IntoStringError},
4 fmt,
5 os::raw::{c_char, c_ulong, c_ushort},
6 ptr,
7 sync::{Arc, Mutex},
8};
9
10use super::{super::atoms::*, ffi, util, XConnection, XError};
11use once_cell::sync::Lazy;
12use x11rb::protocol::xproto;
13
14static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(Default::default);
15
16unsafe fn open_im(xconn: &Arc<XConnection>, locale_modifiers: &CStr) -> Option<ffi::XIM> {
17 let _lock = GLOBAL_LOCK.lock();
18
19 // XSetLocaleModifiers returns...
20 // * The current locale modifiers if it's given a NULL pointer.
21 // * The new locale modifiers if we succeeded in setting them.
22 // * NULL if the locale modifiers string is malformed or if the
23 // current locale is not supported by Xlib.
24 unsafe { (xconn.xlib.XSetLocaleModifiers)(locale_modifiers.as_ptr()) };
25
26 let im = unsafe {
27 (xconn.xlib.XOpenIM)(
28 xconn.display,
29 ptr::null_mut(),
30 ptr::null_mut(),
31 ptr::null_mut(),
32 )
33 };
34
35 if im.is_null() {
36 None
37 } else {
38 Some(im)
39 }
40}
41
42#[derive(Debug)]
43pub struct InputMethod {
44 pub im: ffi::XIM,
45 pub preedit_style: Style,
46 pub none_style: Style,
47 _name: String,
48}
49
50impl InputMethod {
51 fn new(xconn: &Arc<XConnection>, im: ffi::XIM, name: String) -> Option<Self> {
52 let mut styles: *mut XIMStyles = std::ptr::null_mut();
53
54 // Query the styles supported by the XIM.
55 unsafe {
56 if !(xconn.xlib.XGetIMValues)(
57 im,
58 ffi::XNQueryInputStyle_0.as_ptr() as *const _,
59 (&mut styles) as *mut _,
60 std::ptr::null_mut::<()>(),
61 )
62 .is_null()
63 {
64 return None;
65 }
66 }
67
68 let mut preedit_style = None;
69 let mut none_style = None;
70
71 unsafe {
72 std::slice::from_raw_parts((*styles).supported_styles, (*styles).count_styles as _)
73 .iter()
74 .for_each(|style| match *style {
75 XIM_PREEDIT_STYLE => {
76 preedit_style = Some(Style::Preedit(*style));
77 }
78 XIM_NOTHING_STYLE if preedit_style.is_none() => {
79 preedit_style = Some(Style::Nothing(*style))
80 }
81 XIM_NONE_STYLE => none_style = Some(Style::None(*style)),
82 _ => (),
83 });
84
85 (xconn.xlib.XFree)(styles.cast());
86 };
87
88 if preedit_style.is_none() && none_style.is_none() {
89 return None;
90 }
91
92 let preedit_style = preedit_style.unwrap_or_else(|| none_style.unwrap());
93 let none_style = none_style.unwrap_or(preedit_style);
94
95 Some(InputMethod {
96 im,
97 _name: name,
98 preedit_style,
99 none_style,
100 })
101 }
102}
103
104const XIM_PREEDIT_STYLE: XIMStyle = (ffi::XIMPreeditCallbacks | ffi::XIMStatusNothing) as XIMStyle;
105const XIM_NOTHING_STYLE: XIMStyle = (ffi::XIMPreeditNothing | ffi::XIMStatusNothing) as XIMStyle;
106const XIM_NONE_STYLE: XIMStyle = (ffi::XIMPreeditNone | ffi::XIMStatusNone) as XIMStyle;
107
108/// Style of the IME context.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum Style {
111 /// Preedit callbacks.
112 Preedit(XIMStyle),
113
114 /// Nothing.
115 Nothing(XIMStyle),
116
117 /// No IME.
118 None(XIMStyle),
119}
120
121impl Default for Style {
122 fn default() -> Self {
123 Style::None(XIM_NONE_STYLE)
124 }
125}
126
127#[repr(C)]
128#[derive(Debug)]
129struct XIMStyles {
130 count_styles: c_ushort,
131 supported_styles: *const XIMStyle,
132}
133
134pub(crate) type XIMStyle = c_ulong;
135
136#[derive(Debug)]
137pub enum InputMethodResult {
138 /// Input method used locale modifier from `XMODIFIERS` environment variable.
139 XModifiers(InputMethod),
140 /// Input method used internal fallback locale modifier.
141 Fallback(InputMethod),
142 /// Input method could not be opened using any locale modifier tried.
143 Failure,
144}
145
146impl InputMethodResult {
147 pub fn is_fallback(&self) -> bool {
148 matches!(self, InputMethodResult::Fallback(_))
149 }
150
151 pub fn ok(self) -> Option<InputMethod> {
152 use self::InputMethodResult::*;
153 match self {
154 XModifiers(im: InputMethod) | Fallback(im: InputMethod) => Some(im),
155 Failure => None,
156 }
157 }
158}
159
160#[derive(Debug, Clone)]
161enum GetXimServersError {
162 XError(#[allow(dead_code)] XError),
163 GetPropertyError(#[allow(dead_code)] util::GetPropertyError),
164 InvalidUtf8(#[allow(dead_code)] IntoStringError),
165}
166
167impl From<util::GetPropertyError> for GetXimServersError {
168 fn from(error: util::GetPropertyError) -> Self {
169 GetXimServersError::GetPropertyError(error)
170 }
171}
172
173// The root window has a property named XIM_SERVERS, which contains a list of atoms represeting
174// the availabile XIM servers. For instance, if you're using ibus, it would contain an atom named
175// "@server=ibus". It's possible for this property to contain multiple atoms, though presumably
176// rare. Note that we replace "@server=" with "@im=" in order to match the format of locale
177// modifiers, since we don't want a user who's looking at logs to ask "am I supposed to set
178// XMODIFIERS to `@server=ibus`?!?"
179unsafe fn get_xim_servers(xconn: &Arc<XConnection>) -> Result<Vec<String>, GetXimServersError> {
180 let atoms = xconn.atoms();
181 let servers_atom = atoms[XIM_SERVERS];
182
183 let root = unsafe { (xconn.xlib.XDefaultRootWindow)(xconn.display) };
184
185 let mut atoms: Vec<ffi::Atom> = xconn
186 .get_property::<xproto::Atom>(
187 root as xproto::Window,
188 servers_atom,
189 xproto::Atom::from(xproto::AtomEnum::ATOM),
190 )
191 .map_err(GetXimServersError::GetPropertyError)?
192 .into_iter()
193 .map(ffi::Atom::from)
194 .collect::<Vec<_>>();
195
196 let mut names: Vec<*const c_char> = Vec::with_capacity(atoms.len());
197 unsafe {
198 (xconn.xlib.XGetAtomNames)(
199 xconn.display,
200 atoms.as_mut_ptr(),
201 atoms.len() as _,
202 names.as_mut_ptr() as _,
203 )
204 };
205 unsafe { names.set_len(atoms.len()) };
206
207 let mut formatted_names = Vec::with_capacity(names.len());
208 for name in names {
209 let string = unsafe { CStr::from_ptr(name) }
210 .to_owned()
211 .into_string()
212 .map_err(GetXimServersError::InvalidUtf8)?;
213 unsafe { (xconn.xlib.XFree)(name as _) };
214 formatted_names.push(string.replace("@server=", "@im="));
215 }
216 xconn.check_errors().map_err(GetXimServersError::XError)?;
217 Ok(formatted_names)
218}
219
220#[derive(Clone)]
221struct InputMethodName {
222 c_string: CString,
223 string: String,
224}
225
226impl InputMethodName {
227 pub fn from_string(string: String) -> Self {
228 let c_string: CString = CString::new(string.clone())
229 .expect(msg:"String used to construct CString contained null byte");
230 InputMethodName { c_string, string }
231 }
232
233 pub fn from_str(string: &str) -> Self {
234 let c_string: CString =
235 CString::new(string).expect(msg:"String used to construct CString contained null byte");
236 InputMethodName {
237 c_string,
238 string: string.to_owned(),
239 }
240 }
241}
242
243impl fmt::Debug for InputMethodName {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 self.string.fmt(f)
246 }
247}
248
249#[derive(Debug, Clone)]
250struct PotentialInputMethod {
251 name: InputMethodName,
252 successful: Option<bool>,
253}
254
255impl PotentialInputMethod {
256 pub fn from_string(string: String) -> Self {
257 PotentialInputMethod {
258 name: InputMethodName::from_string(string),
259 successful: None,
260 }
261 }
262
263 pub fn from_str(string: &str) -> Self {
264 PotentialInputMethod {
265 name: InputMethodName::from_str(string),
266 successful: None,
267 }
268 }
269
270 pub fn reset(&mut self) {
271 self.successful = None;
272 }
273
274 pub fn open_im(&mut self, xconn: &Arc<XConnection>) -> Option<InputMethod> {
275 let im = unsafe { open_im(xconn, &self.name.c_string) };
276 self.successful = Some(im.is_some());
277 im.and_then(|im| InputMethod::new(xconn, im, self.name.string.clone()))
278 }
279}
280
281// By logging this struct, you get a sequential listing of every locale modifier tried, where it
282// came from, and if it succeeded.
283#[derive(Debug, Clone)]
284pub(crate) struct PotentialInputMethods {
285 // On correctly configured systems, the XMODIFIERS environment variable tells us everything we
286 // need to know.
287 xmodifiers: Option<PotentialInputMethod>,
288 // We have some standard options at our disposal that should ostensibly always work. For users
289 // who only need compose sequences, this ensures that the program launches without a hitch
290 // For users who need more sophisticated IME features, this is more or less a silent failure.
291 // Logging features should be added in the future to allow both audiences to be effectively
292 // served.
293 fallbacks: [PotentialInputMethod; 2],
294 // For diagnostic purposes, we include the list of XIM servers that the server reports as
295 // being available.
296 _xim_servers: Result<Vec<String>, GetXimServersError>,
297}
298
299impl PotentialInputMethods {
300 pub fn new(xconn: &Arc<XConnection>) -> Self {
301 let xmodifiers = env::var("XMODIFIERS")
302 .ok()
303 .map(PotentialInputMethod::from_string);
304 PotentialInputMethods {
305 // Since passing "" to XSetLocaleModifiers results in it defaulting to the value of
306 // XMODIFIERS, it's worth noting what happens if XMODIFIERS is also "". If simply
307 // running the program with `XMODIFIERS="" cargo run`, then assuming XMODIFIERS is
308 // defined in the profile (or parent environment) then that parent XMODIFIERS is used.
309 // If that XMODIFIERS value is also "" (i.e. if you ran `export XMODIFIERS=""`), then
310 // XSetLocaleModifiers uses the default local input method. Note that defining
311 // XMODIFIERS as "" is different from XMODIFIERS not being defined at all, since in
312 // that case, we get `None` and end up skipping ahead to the next method.
313 xmodifiers,
314 fallbacks: [
315 // This is a standard input method that supports compose sequences, which should
316 // always be available. `@im=none` appears to mean the same thing.
317 PotentialInputMethod::from_str("@im=local"),
318 // This explicitly specifies to use the implementation-dependent default, though
319 // that seems to be equivalent to just using the local input method.
320 PotentialInputMethod::from_str("@im="),
321 ],
322 // The XIM_SERVERS property can have surprising values. For instance, when I exited
323 // ibus to run fcitx, it retained the value denoting ibus. Even more surprising is
324 // that the fcitx input method could only be successfully opened using "@im=ibus".
325 // Presumably due to this quirk, it's actually possible to alternate between ibus and
326 // fcitx in a running application.
327 _xim_servers: unsafe { get_xim_servers(xconn) },
328 }
329 }
330
331 // This resets the `successful` field of every potential input method, ensuring we have
332 // accurate information when this struct is re-used by the destruction/instantiation callbacks.
333 fn reset(&mut self) {
334 if let Some(ref mut input_method) = self.xmodifiers {
335 input_method.reset();
336 }
337
338 for input_method in &mut self.fallbacks {
339 input_method.reset();
340 }
341 }
342
343 pub fn open_im(
344 &mut self,
345 xconn: &Arc<XConnection>,
346 callback: Option<&dyn Fn()>,
347 ) -> InputMethodResult {
348 use self::InputMethodResult::*;
349
350 self.reset();
351
352 if let Some(ref mut input_method) = self.xmodifiers {
353 let im = input_method.open_im(xconn);
354 if let Some(im) = im {
355 return XModifiers(im);
356 } else if let Some(ref callback) = callback {
357 callback();
358 }
359 }
360
361 for input_method in &mut self.fallbacks {
362 let im = input_method.open_im(xconn);
363 if let Some(im) = im {
364 return Fallback(im);
365 }
366 }
367
368 Failure
369 }
370}
371