use std::collections::VecDeque;
use zbus::blocking::Connection;
use crate::{Error, InputMethodEvent, InputMethodState, Result};
const IBUS_SERVICE: &str = "org.freedesktop.IBus";
const IBUS_PATH: &str = "/org/freedesktop/IBus";
const IBUS_INTERFACE: &str = "org.freedesktop.IBus";
const IBUS_INPUT_CONTEXT_INTERFACE: &str = "org.freedesktop.IBus.InputContext";
pub struct InputMethod {
connection: Connection,
input_context_path: Option<String>,
state: InputMethodState,
events: VecDeque<InputMethodEvent>,
serial: u32,
}
impl InputMethod {
pub fn new() -> Result<Self> {
let connection = Connection::session()
.map_err(|e| Error::IBus(format!("Failed to connect to D-Bus session bus: {}", e)))?;
let proxy = connection.call_method(
Some("org.freedesktop.DBus"),
"/org/freedesktop/DBus",
Some("org.freedesktop.DBus"),
"NameHasOwner",
&(IBUS_SERVICE,),
);
let has_owner: bool = match proxy {
Ok(reply) => reply
.body()
.deserialize()
.map_err(|e| Error::IBus(format!("Failed to check IBus service: {}", e)))?,
Err(e) => {
return Err(Error::IBus(format!("Failed to query D-Bus: {}", e)));
}
};
if !has_owner {
return Err(Error::IBus("IBus service is not running".to_string()));
}
let mut im = Self {
connection,
input_context_path: None,
state: InputMethodState::new(),
events: VecDeque::new(),
serial: 0,
};
im.create_input_context()?;
Ok(im)
}
fn create_input_context(&mut self) -> Result<()> {
let reply = self
.connection
.call_method(
Some(IBUS_SERVICE),
IBUS_PATH,
Some(IBUS_INTERFACE),
"CreateInputContext",
&("imekit",),
)
.map_err(|e| Error::IBus(format!("Failed to create input context: {}", e)))?;
let context_path: String = reply
.body()
.deserialize()
.map_err(|e| Error::IBus(format!("Failed to parse input context path: {}", e)))?;
log_debug!("Created IBus input context: {}", context_path);
self.input_context_path = Some(context_path);
self.focus_in()?;
Ok(())
}
fn focus_in(&mut self) -> Result<()> {
if let Some(ref path) = self.input_context_path {
self.connection
.call_method(
Some(IBUS_SERVICE),
path.as_str(),
Some(IBUS_INPUT_CONTEXT_INTERFACE),
"FocusIn",
&(),
)
.map_err(|e| Error::IBus(format!("Failed to focus in: {}", e)))?;
self.state.active = true;
self.serial += 1;
self.state.serial = self.serial;
self.events.push_back(InputMethodEvent::Activate {
serial: self.serial,
});
}
Ok(())
}
fn focus_out(&mut self) -> Result<()> {
if let Some(ref path) = self.input_context_path {
self.connection
.call_method(
Some(IBUS_SERVICE),
path.as_str(),
Some(IBUS_INPUT_CONTEXT_INTERFACE),
"FocusOut",
&(),
)
.map_err(|e| Error::IBus(format!("Failed to focus out: {}", e)))?;
self.state.active = false;
self.events.push_back(InputMethodEvent::Deactivate);
}
Ok(())
}
pub fn next_event(&mut self) -> Option<InputMethodEvent> {
if let Some(event) = self.events.pop_front() {
return Some(event);
}
None
}
pub fn is_active(&self) -> bool {
self.state.active
}
pub fn commit_string(&self, text: &str) -> Result<()> {
if self.input_context_path.is_some() {
self.commit_string_via_keys(text)
} else {
Err(Error::NotActive)
}
}
fn commit_string_via_keys(&self, text: &str) -> Result<()> {
if let Some(ref path) = self.input_context_path {
for c in text.chars() {
let keyval = char_to_ibus_keyval(c);
if keyval != 0 {
let _: bool = self
.connection
.call_method(
Some(IBUS_SERVICE),
path.as_str(),
Some(IBUS_INPUT_CONTEXT_INTERFACE),
"ProcessKeyEvent",
&(keyval, 0u32, 0u32), )
.map_err(|e| Error::IBus(format!("ProcessKeyEvent failed: {}", e)))?
.body()
.deserialize()
.map_err(|e| Error::IBus(format!("Failed to parse response: {}", e)))?;
let _: bool = self
.connection
.call_method(
Some(IBUS_SERVICE),
path.as_str(),
Some(IBUS_INPUT_CONTEXT_INTERFACE),
"ProcessKeyEvent",
&(keyval, 0u32, 0x80000000u32), )
.map_err(|e| Error::IBus(format!("ProcessKeyEvent release failed: {}", e)))?
.body()
.deserialize()
.map_err(|e| Error::IBus(format!("Failed to parse response: {}", e)))?;
}
}
Ok(())
} else {
Err(Error::NotActive)
}
}
pub fn set_preedit_string(&self, text: &str, cursor_begin: i32, cursor_end: i32) -> Result<()> {
if let Some(ref _path) = self.input_context_path {
log_debug!(
"IBus preedit: {} (cursor: {}-{})",
text,
cursor_begin,
cursor_end
);
Ok(())
} else {
Err(Error::NotActive)
}
}
pub fn delete_surrounding_text(&self, before: u32, after: u32) -> Result<()> {
if let Some(ref path) = self.input_context_path {
if before > 0 {
self.connection
.call_method(
Some(IBUS_SERVICE),
path.as_str(),
Some(IBUS_INPUT_CONTEXT_INTERFACE),
"DeleteSurroundingText",
&(-(before as i32), before),
)
.map_err(|e| {
Error::IBus(format!("DeleteSurroundingText (before) failed: {}", e))
})?;
}
if after > 0 {
self.connection
.call_method(
Some(IBUS_SERVICE),
path.as_str(),
Some(IBUS_INPUT_CONTEXT_INTERFACE),
"DeleteSurroundingText",
&(0i32, after),
)
.map_err(|e| {
Error::IBus(format!("DeleteSurroundingText (after) failed: {}", e))
})?;
}
Ok(())
} else {
Err(Error::NotActive)
}
}
pub fn commit(&self, _serial: u32) -> Result<()> {
Ok(())
}
pub fn state(&self) -> InputMethodState {
self.state.clone()
}
}
impl Drop for InputMethod {
fn drop(&mut self) {
let _ = self.focus_out();
}
}
fn char_to_ibus_keyval(c: char) -> u32 {
let code = c as u32;
if (0x20..=0x7E).contains(&code) {
return code;
}
if (0xA0..=0xFF).contains(&code) {
return code;
}
if code > 0xFF {
return 0x0100_0000 | code;
}
0
}