diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 898ddd2e..fc1bfcec 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1040,6 +1040,10 @@ impl ConfigClient { self.send(&ClientMessage::SeatReloadSimpleIm { seat }); } + pub fn seat_enable_unicode_input(&self, seat: Seat) { + self.send(&ClientMessage::SeatEnableUnicodeInput { seat }); + } + pub fn set_show_float_pin_icon(&self, show: bool) { self.send(&ClientMessage::SetShowFloatPinIcon { show }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index b1e91ec4..746e91c0 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -798,6 +798,9 @@ pub enum ClientMessage<'a> { SeatReloadSimpleIm { seat: Seat, }, + SeatEnableUnicodeInput { + seat: Seat, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 1956791b..4672663c 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -631,6 +631,13 @@ impl Seat { pub fn reload_simple_im(self) { get!().seat_reload_simple_im(self); } + + /// Enables Unicode input in the simple, XCompose based input method. + /// + /// This has no effect if the simple IM is not currently active. + pub fn enable_unicode_input(self) { + get!().seat_enable_unicode_input(self); + } } /// A focus-follows-mouse mode. diff --git a/src/config/handler.rs b/src/config/handler.rs index c1cd7759..db46821a 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2301,6 +2301,12 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_seat_enable_unicode_input(&self, seat: Seat) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.enable_unicode_input(); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -3245,6 +3251,9 @@ impl ConfigProxyHandler { ClientMessage::SeatReloadSimpleIm { seat } => self .handle_seat_reload_simple_im(seat) .wrn("seat_reload_simple_im")?, + ClientMessage::SeatEnableUnicodeInput { seat } => self + .handle_seat_enable_unicode_input(seat) + .wrn("seat_enable_unicode_input")?, } Ok(()) } diff --git a/src/ifs/wl_seat/text_input.rs b/src/ifs/wl_seat/text_input.rs index 2cdf08bf..51cea1d3 100644 --- a/src/ifs/wl_seat/text_input.rs +++ b/src/ifs/wl_seat/text_input.rs @@ -42,6 +42,7 @@ pub trait InputMethod { fn done(self: Rc, seat: &WlSeatGlobal); fn is_simple(&self) -> bool; fn cancel_simple(&self, seat: &WlSeatGlobal); + fn enable_unicode_input(&self); } pub trait InputMethodKeyboardGrab { @@ -64,6 +65,12 @@ pub enum TextDisconnectReason { } impl WlSeatGlobal { + pub fn enable_unicode_input(&self) { + if let Some(im) = self.input_method.get() { + im.enable_unicode_input(); + } + } + pub fn set_simple_im_enabled(self: &Rc, enabled: bool) { if self.simple_im_enabled.replace(enabled) == enabled { return; diff --git a/src/ifs/wl_seat/text_input/simple_im.rs b/src/ifs/wl_seat/text_input/simple_im.rs index 589d2c11..e7f03188 100644 --- a/src/ifs/wl_seat/text_input/simple_im.rs +++ b/src/ifs/wl_seat/text_input/simple_im.rs @@ -24,6 +24,7 @@ use { }, std::{ cell::{Cell, RefCell}, + fmt::Write, rc::Rc, }, }; @@ -36,6 +37,8 @@ pub struct SimpleIm { table: compose::ComposeTable, initial_state: compose::State, states: RefCell>, + unicode_input: RefCell, + unicode_input_enabled: Cell, } struct State { @@ -43,6 +46,14 @@ struct State { char: char, } +#[derive(Default)] +struct UnicodeInput { + text: String, + cp: u32, + cursor: i32, + chars: usize, +} + impl SimpleIm { pub fn new(ctx: &xkb::Context) -> Option> { let table = ctx.compose_table_builder().build(WriteToLog)?; @@ -54,6 +65,8 @@ impl SimpleIm { states: Default::default(), initial_state: table.create_state(), table, + unicode_input: Default::default(), + unicode_input_enabled: Default::default(), })) } } @@ -94,6 +107,7 @@ impl InputMethod for SimpleIm { self.active.set(active); if active { self.states.borrow_mut().clear(); + self.unicode_input_enabled.set(false); seat.input_method_grab.set(Some(self)); } else { seat.input_method_grab.take(); @@ -110,6 +124,46 @@ impl InputMethod for SimpleIm { con.disconnect(TextDisconnectReason::InputMethodDestroyed); } } + + fn enable_unicode_input(&self) { + if !self.active.get() { + return; + } + let Some(con) = self.con.get() else { + return; + }; + if self.unicode_input_enabled.replace(true) { + return; + } + self.states.borrow_mut().clear(); + let ui = &mut *self.unicode_input.borrow_mut(); + ui.cp = 0; + ui.chars = 0; + ui.flush_preedit(&con); + } +} + +impl UnicodeInput { + fn update_text(&mut self) { + self.text.clear(); + if self.chars == 0 { + let _ = write!(self.text, "U+"); + self.cursor = self.text.len() as _; + return; + } + let _ = write!(self.text, "U+{:x}", self.cp); + self.cursor = self.text.len() as _; + if let Some(char) = char::from_u32(self.cp) { + let _ = write!(self.text, " = {}", char); + } + } + + fn flush_preedit(&mut self, con: &TextInputConnection) { + self.update_text(); + con.text_input + .send_preedit_string(Some(&self.text), self.cursor, self.cursor); + con.text_input.send_done(); + } } impl InputMethodKeyboardGrab for SimpleIm { @@ -122,7 +176,11 @@ impl InputMethodKeyboardGrab for SimpleIm { }; let mut buf = [0; 4]; let mut forward_to_node = true; + if self.unicode_input_enabled.get() { + forward_to_node = false; + } let states = &mut *self.states.borrow_mut(); + let ui = &mut self.unicode_input.borrow_mut(); let lookup = kb_state.map.lookup_table.lookup( kb_state.mods.group, kb_state.mods.mods, @@ -132,6 +190,53 @@ impl InputMethodKeyboardGrab for SimpleIm { let is_control = mods.contains(ModifierMask::CONTROL); for sym in lookup { let sym = sym.keysym(); + if self.unicode_input_enabled.get() { + let is_terminator = matches!( + sym, + syms::Return | syms::KP_Enter | syms::space | syms::KP_Space + ); + if (is_terminator || (sym == syms::j && is_control)) + && ui.chars > 0 + && let Some(char) = char::from_u32(ui.cp) + { + self.unicode_input_enabled.set(false); + let s = char.encode_utf8(&mut buf); + con.text_input.send_preedit_string(None, 0, 0); + con.text_input.send_commit_string(Some(s)); + con.text_input.send_done(); + } else if sym == syms::Escape + || (sym == syms::c && is_control) + || (ui.chars == 0 && matches!(sym, syms::w | syms::d) && is_control) + { + self.unicode_input_enabled.set(false); + con.text_input.send_preedit_string(None, 0, 0); + con.text_input.send_done(); + } else if sym == syms::BackSpace && ui.chars > 0 { + ui.chars -= 1; + ui.cp >>= 4; + ui.flush_preedit(&con); + } else if sym == syms::w && is_control { + ui.chars = 0; + ui.cp = 0; + ui.flush_preedit(&con); + } else if let Some(c) = sym.char() + && ui.chars < 6 + && !is_control + { + let c = match c { + '0'..='9' => c as u32 - '0' as u32, + 'a'..='f' => c as u32 - 'a' as u32 + 10, + 'A'..='F' => c as u32 - 'A' as u32 + 10, + _ => continue, + }; + ui.cp = (ui.cp << 4) | c; + if ui.cp != 0 { + ui.chars += 1; + ui.flush_preedit(&con); + } + } + continue; + } let mut new_state = states .last() .map(|s| s.state.clone()) diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs index fe82d42a..35c4467a 100644 --- a/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs +++ b/src/ifs/wl_seat/text_input/zwp_input_method_v2.rs @@ -144,6 +144,10 @@ impl InputMethod for ZwpInputMethodV2 { fn cancel_simple(&self, _seat: &WlSeatGlobal) { unreachable!(); } + + fn enable_unicode_input(&self) { + // nothing + } } impl ZwpInputMethodV2RequestHandler for ZwpInputMethodV2 { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 5c7785e8..d77948d4 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -88,6 +88,7 @@ pub enum SimpleCommand { EnableSimpleIm(bool), ToggleSimpleImEnabled, ReloadSimpleIm, + EnableUnicodeInput, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 1f1bd327..9063fc89 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -159,6 +159,7 @@ impl ActionParser<'_> { "disable-simple-im" => EnableSimpleIm(false), "toggle-simple-im-enabled" => ToggleSimpleImEnabled, "reload-simple-im" => ReloadSimpleIm, + "enable-unicode-input" => EnableUnicodeInput, _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 7ba79aeb..b4d99fdb 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -236,6 +236,10 @@ impl Action { let persistent = state.persistent.clone(); b.new(move || persistent.seat.reload_simple_im()) } + SimpleCommand::EnableUnicodeInput => { + let persistent = state.persistent.clone(); + b.new(move || persistent.seat.enable_unicode_input()) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index a2fa94e9..6f5df501 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1843,7 +1843,8 @@ "enable-simple-im", "disable-simple-im", "toggle-simple-im-enabled", - "reload-simple-im" + "reload-simple-im", + "enable-unicode-input" ] }, "SimpleIm": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index e4666c24..43a32c02 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4216,6 +4216,12 @@ The string should have one of the following values: This is useful if you change the XCompose files after starting the compositor. +- `enable-unicode-input`: + + Enables Unicode input in the simple, XCompose based input method. + + This has no effect if the simple IM is not currently active. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 4262deff..32d122bc 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1050,6 +1050,11 @@ SimpleActionName: Reloads the simple, XCompose based input method. This is useful if you change the XCompose files after starting the compositor. + - value: enable-unicode-input + description: | + Enables Unicode input in the simple, XCompose based input method. + + This has no effect if the simple IM is not currently active. Color: