diff --git a/Cargo.toml b/Cargo.toml index 381eddf5..a4e37d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ linearize = { version = "0.1.3", features = ["derive"] } png = "0.18.0" rustc-demangle = { version = "0.1.24", optional = true } tracy-client-sys = { version = "0.24.1", features = ["ondemand", "manual-lifetime", "debuginfod", "demangle"], optional = true } -kbvm = "0.1.5" +kbvm = { version = "0.1.5", features = ["compose"] } tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] } regex = "1.11.1" cfg-if = "1.0.0" diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 936b8b5c..fc1bfcec 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1026,6 +1026,24 @@ impl ConfigClient { self.send(&ClientMessage::SeatCopyMark { seat, src, dst }); } + pub fn seat_set_simple_im_enabled(&self, seat: Seat, enabled: bool) { + self.send(&ClientMessage::SeatSetSimpleImEnabled { seat, enabled }); + } + + pub fn seat_get_simple_im_enabled(&self, seat: Seat) -> bool { + let res = self.send_with_response(&ClientMessage::SeatGetSimpleImEnabled { seat }); + get_response!(res, false, SeatGetSimpleImEnabled { enabled }); + enabled + } + + pub fn seat_reload_simple_im(&self, seat: Seat) { + 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 02e9fbfd..746e91c0 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -788,6 +788,19 @@ pub enum ClientMessage<'a> { workspace: Workspace, connector: Connector, }, + SeatSetSimpleImEnabled { + seat: Seat, + enabled: bool, + }, + SeatGetSimpleImEnabled { + seat: Seat, + }, + SeatReloadSimpleIm { + seat: Seat, + }, + SeatEnableUnicodeInput { + seat: Seat, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -1020,6 +1033,9 @@ pub enum Response { GetShowBar { show: bool, }, + SeatGetSimpleImEnabled { + enabled: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 61b6c484..4672663c 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -603,6 +603,41 @@ impl Seat { pub fn copy_mark(self, src: u32, dst: u32) { get!().seat_copy_mark(self, src, dst); } + + /// Sets whether the simple, XCompose based input method is enabled. + /// + /// Regardless of this setting, this input method is not used if an external input + /// method is running. + /// + /// The default is `true`. + pub fn set_simple_im_enabled(self, enabled: bool) { + get!().seat_set_simple_im_enabled(self, enabled); + } + + /// Returns whether the simple, XCompose based input method is enabled. + pub fn simple_im_enabled(self) -> bool { + get!(true).seat_get_simple_im_enabled(self) + } + + /// Toggles whether the simple, XCompose based input method is enabled. + pub fn toggle_simple_im_enabled(self) { + let get = get!(); + get.seat_set_simple_im_enabled(self, !get.seat_get_simple_im_enabled(self)); + } + + /// Reloads the simple, XCompose based input method. + /// + /// This is useful if you change the XCompose files after starting the compositor. + 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/cli/input.rs b/src/cli/input.rs index aaf75aa5..ee253ebf 100644 --- a/src/cli/input.rs +++ b/src/cli/input.rs @@ -83,6 +83,28 @@ pub enum SeatCommand { UseHardwareCursor(UseHardwareCursorArgs), /// Set the size of the cursor. SetCursorSize(SetCursorSizeArgs), + /// Configure the simple, XCompose based input method. + SimpleIm(SimpleImArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct SimpleImArgs { + #[clap(subcommand)] + pub command: SimpleImCommand, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum SimpleImCommand { + /// Enable the simple IM. + /// + /// Even if the IM is enabled, it will not be used if an external IM is running. + Enable, + /// Disable the simple IM. + Disable, + /// Reload the simple IM. + /// + /// This is useful if you change the XCompose files after starting the compositor. + Reload, } impl Default for SeatCommand { @@ -460,6 +482,27 @@ impl Input { size: a.size, }); } + SeatCommand::SimpleIm(a) => match a.command { + SimpleImCommand::Enable | SimpleImCommand::Disable => { + self.handle_error(input, |e| { + eprintln!("Could not enable/disable the simple IM: {}", e); + }); + tc.send(jay_input::SetSimpleImEnabled { + self_id: input, + seat: &args.seat, + enabled: matches!(a.command, SimpleImCommand::Enable) as _, + }); + } + SimpleImCommand::Reload => { + self.handle_error(input, |e| { + eprintln!("Could not reload the simple IM: {}", e); + }); + tc.send(jay_input::ReloadSimpleIm { + self_id: input, + seat: &args.seat, + }); + } + }, } tc.round_trip().await; } diff --git a/src/config/handler.rs b/src/config/handler.rs index 8573532e..db46821a 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2281,6 +2281,32 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_seat_set_simple_im_enabled(&self, seat: Seat, enabled: bool) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.set_simple_im_enabled(enabled); + Ok(()) + } + + fn handle_seat_get_simple_im_enabled(&self, seat: Seat) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + self.respond(Response::SeatGetSimpleImEnabled { + enabled: seat.simple_im_enabled(), + }); + Ok(()) + } + + fn handle_seat_reload_simple_im(&self, seat: Seat) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.reload_simple_im(); + 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 { @@ -3216,6 +3242,18 @@ impl ConfigProxyHandler { } => self .handle_show_workspace(seat, workspace, Some(connector)) .wrn("show_workspace_on")?, + ClientMessage::SeatSetSimpleImEnabled { seat, enabled } => self + .handle_seat_set_simple_im_enabled(seat, enabled) + .wrn("seat_set_simple_im_enabled")?, + ClientMessage::SeatGetSimpleImEnabled { seat } => self + .handle_seat_get_simple_im_enabled(seat) + .wrn("seat_get_simple_im_enabled")?, + 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/gfx_api.rs b/src/gfx_api.rs index 810bdd81..5193f40b 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -955,7 +955,7 @@ pub fn create_render_pass( for seat in seats.values() { let (x, y) = seat.pointer_cursor().position_int(); if let Some(im) = seat.input_method() { - for (_, popup) in &im.popups { + for (_, popup) in im.popups() { if popup.surface.node_visible() { let pos = popup.surface.buffer_abs_pos.get(); let extents = popup.surface.extents.get().move_(pos.x1(), pos.y1()); diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 05aa3064..7e1dde99 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -79,7 +79,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 21 + 22 } fn required_caps(&self) -> ClientCaps { diff --git a/src/ifs/jay_input.rs b/src/ifs/jay_input.rs index dbcb75e3..531d2cd3 100644 --- a/src/ifs/jay_input.rs +++ b/src/ifs/jay_input.rs @@ -515,6 +515,30 @@ impl JayInputRequestHandler for JayInput { Ok(()) }) } + + fn set_simple_im_enabled( + &self, + req: SetSimpleImEnabled<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.or_error(|| { + let seat = self.seat(req.seat)?; + seat.set_simple_im_enabled(req.enabled != 0); + Ok(()) + }) + } + + fn reload_simple_im( + &self, + req: ReloadSimpleIm<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + self.or_error(|| { + let seat = self.seat(req.seat)?; + seat.reload_simple_im(); + Ok(()) + }) + } } object_base! { diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index c89853d9..a2e3200e 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -52,8 +52,8 @@ use { pointer_owner::PointerOwnerHolder, tablet::TabletSeatData, text_input::{ - zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2, - zwp_input_method_v2::ZwpInputMethodV2, zwp_text_input_v3::ZwpTextInputV3, + InputMethod, InputMethodKeyboardGrab, simple_im::SimpleIm, + zwp_text_input_v3::ZwpTextInputV3, }, touch_owner::TouchOwnerHolder, wl_keyboard::{REPEAT_INFO_SINCE, WlKeyboard, WlKeyboardError}, @@ -216,8 +216,8 @@ pub struct WlSeatGlobal { last_input_usec: Cell, text_inputs: RefCell>>>, text_input: CloneCell>>, - input_method: CloneCell>>, - input_method_grab: CloneCell>>, + input_method: CloneCell>>, + input_method_grab: CloneCell>>, forward: Cell, focus_follows_mouse: Cell, swipe_bindings: PerClientBindings, @@ -238,6 +238,8 @@ pub struct WlSeatGlobal { marks: CopyHashMap>, modifiers_listener: EventListener, modifiers_forward: EventSource, + simple_im: CloneCell>>, + simple_im_enabled: Cell, } #[derive(Copy, Clone)] @@ -259,6 +261,7 @@ impl WlSeatGlobal { let cursor_user_group = CursorUserGroup::create(state); let cursor_user = cursor_user_group.create_user(); cursor_user.activate(); + let simple_im = SimpleIm::new(&state.kb_ctx.ctx); let slf = Rc::new_cyclic(|slf: &Weak| Self { id: state.seat_ids.next(), name, @@ -306,7 +309,7 @@ impl WlSeatGlobal { data_control_devices: Default::default(), text_inputs: Default::default(), text_input: Default::default(), - input_method: Default::default(), + input_method: CloneCell::new(simple_im.clone().map(|im| im as _)), input_method_grab: Default::default(), forward: Cell::new(false), focus_follows_mouse: Cell::new(true), @@ -327,6 +330,8 @@ impl WlSeatGlobal { marks: Default::default(), modifiers_listener: EventListener::new(slf.clone()), modifiers_forward: Default::default(), + simple_im: CloneCell::new(simple_im), + simple_im_enabled: Cell::new(true), }); slf.pointer_cursor.set_owner(slf.clone()); slf.modifiers_listener @@ -371,7 +376,7 @@ impl WlSeatGlobal { self.seat_kb_map.get() } - pub fn input_method(&self) -> Option> { + pub fn input_method(&self) -> Option> { self.input_method.get() } @@ -693,7 +698,7 @@ impl WlSeatGlobal { } } if let Some(grab) = self.input_method_grab.get() { - grab.send_repeat_info(); + grab.on_repeat_info(); } } @@ -1315,7 +1320,7 @@ impl WlSeatGlobal { tl.tl_set_visible(visible); } if let Some(im) = self.input_method.get() { - for (_, popup) in &im.popups { + for (_, popup) in im.popups() { popup.update_visible(); } } diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 616c9d3b..8cb052b7 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -981,15 +981,19 @@ impl WlSeatGlobal { } } self.send_components(&mut components_changed, &kbvm_state); - match self.input_method_grab.get() { - Some(g) => g.on_key(time_usec, kc.to_evdev(), key_state, &kbvm_state.kb_state), - _ => self.keyboard_node.get().node_on_key( + let mut forward_to_node = true; + if let Some(g) = self.input_method_grab.get() { + forward_to_node = + g.on_key(time_usec, kc.to_evdev(), key_state, &kbvm_state.kb_state); + } + if forward_to_node { + self.keyboard_node.get().node_on_key( self, time_usec, kc.to_evdev(), key_state, &kbvm_state.kb_state, - ), + ) } self.for_each_ei_seat(|ei_seat| { ei_seat.handle_key(time_usec, kc.to_evdev(), key_state, &kbvm_state.kb_state); @@ -1064,9 +1068,12 @@ impl WlSeatGlobal { self.state.for_each_seat_tester(|t| { t.send_modifiers(self.id, &kb_state.mods); }); - match self.input_method_grab.get() { - Some(g) => g.on_modifiers(kb_state), - _ => self.keyboard_node.get().node_on_mods(self, kb_state), + let mut forward_to_node = true; + if let Some(g) = self.input_method_grab.get() { + forward_to_node = g.on_modifiers(kb_state); + } + if forward_to_node { + self.keyboard_node.get().node_on_mods(self, kb_state) } } diff --git a/src/ifs/wl_seat/text_input.rs b/src/ifs/wl_seat/text_input.rs index 4f4f8418..51cea1d3 100644 --- a/src/ifs/wl_seat/text_input.rs +++ b/src/ifs/wl_seat/text_input.rs @@ -1,16 +1,21 @@ use { - crate::ifs::{ - wl_seat::{ - WlSeatGlobal, - text_input::{ - zwp_input_method_v2::ZwpInputMethodV2, zwp_text_input_v3::ZwpTextInputV3, + crate::{ + backend::KeyState, + ifs::{ + wl_seat::{ + WlSeatGlobal, + text_input::{simple_im::SimpleIm, zwp_text_input_v3::ZwpTextInputV3}, }, + wl_surface::{WlSurface, zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2}, }, - wl_surface::WlSurface, + keyboard::KeyboardState, + utils::smallmap::SmallMap, + wire::ZwpInputPopupSurfaceV2Id, }, std::rc::Rc, }; +pub mod simple_im; pub mod zwp_input_method_keyboard_grab_v2; pub mod zwp_input_method_manager_v2; pub mod zwp_input_method_v2; @@ -22,10 +27,30 @@ const MAX_TEXT_SIZE: usize = 4000; pub struct TextInputConnection { pub seat: Rc, pub text_input: Rc, - pub input_method: Rc, + pub input_method: Rc, pub surface: Rc, } +pub trait InputMethod { + fn set_connection(&self, con: Option<&Rc>); + fn popups(&self) -> &SmallMap, 1>; + fn activate(&self); + fn deactivate(&self); + fn content_type(&self, hint: u32, purpose: u32); + fn text_change_cause(&self, cause: u32); + fn surrounding_text(&self, text: &str, cursor: u32, anchor: u32); + 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 { + fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) -> bool; + fn on_modifiers(&self, kb_state: &KeyboardState) -> bool; + fn on_repeat_info(&self); +} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum TextConnectReason { TextInputEnabled, @@ -40,6 +65,76 @@ 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; + } + if enabled { + if self.input_method.is_none() + && let Some(im) = self.simple_im.get() + { + self.set_input_method(im); + } + } else { + if let Some(im) = self.input_method.get() + && im.is_simple() + { + self.input_method.take(); + im.cancel_simple(self); + } + } + } + + pub fn simple_im_enabled(&self) -> bool { + self.simple_im_enabled.get() + } + + pub fn reload_simple_im(self: &Rc) { + let im = SimpleIm::new(&self.state.kb_ctx.ctx); + self.simple_im.set(im.clone()); + if self.simple_im_enabled.get() && self.can_set_new_im() { + if let Some(im) = im { + self.set_input_method(im); + } else if let Some(old) = self.input_method.take() { + old.cancel_simple(self); + } + } + } + + fn can_set_new_im(&self) -> bool { + match self.input_method.get() { + None => true, + Some(im) => im.is_simple(), + } + } + + fn cannot_set_new_im(&self) -> bool { + !self.can_set_new_im() + } + + fn set_input_method(self: &Rc, im: Rc) { + if let Some(old) = self.input_method.take() { + old.cancel_simple(self); + } + self.input_method.set(Some(im)); + self.create_text_input_connection(TextConnectReason::InputMethodCreated); + } + + fn remove_input_method(self: &Rc) { + self.input_method.take(); + if self.simple_im_enabled.get() + && let Some(im) = self.simple_im.get() + { + self.set_input_method(im); + } + } + fn create_text_input_connection(self: &Rc, text_connect_reason: TextConnectReason) { let Some(im) = self.input_method.get() else { return; @@ -67,7 +162,7 @@ impl WlSeatGlobal { impl TextInputConnection { fn connect(self: &Rc, reason: TextConnectReason) { - self.input_method.connection.set(Some(self.clone())); + self.input_method.set_connection(Some(self)); self.text_input.connection.set(Some(self.clone())); self.surface .text_input_connections @@ -75,22 +170,27 @@ impl TextInputConnection { self.input_method.activate(); if reason == TextConnectReason::InputMethodCreated { - self.text_input.send_all_to(&self.input_method); - self.input_method.send_done(); + self.text_input.send_all_to(&*self.input_method); + self.input_method.clone().done(&self.seat); } } pub fn disconnect(&self, reason: TextDisconnectReason) { self.text_input.connection.take(); - self.input_method.connection.take(); + self.input_method.set_connection(None); self.surface.text_input_connections.remove(&self.seat.id); if reason != TextDisconnectReason::InputMethodDestroyed { - self.input_method.send_deactivate(); - self.input_method.send_done(); - for (_, popup) in &self.input_method.popups { + self.input_method.deactivate(); + self.input_method.clone().done(&self.seat); + for (_, popup) in self.input_method.popups() { popup.update_visible(); } } + if reason != TextDisconnectReason::TextInputDisabled { + self.text_input.send_preedit_string(None, 0, 0); + self.text_input.send_commit_string(None); + self.text_input.send_done(); + } } } diff --git a/src/ifs/wl_seat/text_input/simple_im.rs b/src/ifs/wl_seat/text_input/simple_im.rs new file mode 100644 index 00000000..e7f03188 --- /dev/null +++ b/src/ifs/wl_seat/text_input/simple_im.rs @@ -0,0 +1,309 @@ +use { + crate::{ + backend::KeyState, + ifs::{ + wl_seat::{ + WlSeatGlobal, + text_input::{ + InputMethod, InputMethodKeyboardGrab, TextDisconnectReason, TextInputConnection, + }, + }, + wl_surface::zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, + }, + keyboard::KeyboardState, + utils::{clonecell::CloneCell, smallmap::SmallMap}, + wire::ZwpInputPopupSurfaceV2Id, + }, + kbvm::{ + Keycode, ModifierMask, syms, + xkb::{ + self, + compose::{self, FeedResult}, + diagnostic::WriteToLog, + }, + }, + std::{ + cell::{Cell, RefCell}, + fmt::Write, + rc::Rc, + }, +}; + +pub struct SimpleIm { + con: CloneCell>>, + popups: SmallMap, 1>, + active: Cell, + activate: Cell>, + table: compose::ComposeTable, + initial_state: compose::State, + states: RefCell>, + unicode_input: RefCell, + unicode_input_enabled: Cell, +} + +struct State { + state: compose::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)?; + Some(Rc::new(Self { + con: Default::default(), + popups: Default::default(), + active: Default::default(), + activate: Default::default(), + states: Default::default(), + initial_state: table.create_state(), + table, + unicode_input: Default::default(), + unicode_input_enabled: Default::default(), + })) + } +} + +impl InputMethod for SimpleIm { + fn set_connection(&self, con: Option<&Rc>) { + self.con.set(con.cloned()); + } + + fn popups(&self) -> &SmallMap, 1> { + &self.popups + } + + fn activate(&self) { + self.activate.set(Some(true)); + } + + fn deactivate(&self) { + self.activate.set(Some(false)); + } + + fn content_type(&self, _hint: u32, _purpose: u32) { + // nothing + } + + fn text_change_cause(&self, _cause: u32) { + // nothing + } + + fn surrounding_text(&self, _text: &str, _cursor: u32, _anchor: u32) { + // nothing + } + + fn done(self: Rc, seat: &WlSeatGlobal) { + let Some(active) = self.activate.take() else { + return; + }; + 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(); + } + } + + fn is_simple(&self) -> bool { + true + } + + fn cancel_simple(&self, seat: &WlSeatGlobal) { + seat.input_method_grab.take(); + if let Some(con) = self.con.get() { + 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 { + fn on_key(&self, _time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) -> bool { + if state != KeyState::Pressed { + return true; + } + let Some(con) = self.con.get() else { + return true; + }; + 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, + Keycode::from_evdev(key), + ); + let mods = lookup.remaining_mods(); + 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()) + .unwrap_or_else(|| self.initial_state.clone()); + let Some(fr) = self.table.feed(&mut new_state, sym) else { + continue; + }; + forward_to_node = false; + let mut send_preedit = |char: char| { + let s = char.encode_utf8(&mut buf); + let len = s.len() as i32; + con.text_input.send_preedit_string(Some(s), len, len); + }; + match fr { + FeedResult::Pending => { + let char = sym.char().unwrap_or('ยท'); + states.push(State { + state: new_state, + char, + }); + send_preedit(char); + con.text_input.send_done(); + } + FeedResult::Aborted + if sym == syms::Escape || (matches!(sym, syms::c | syms::w) && is_control) => + { + states.clear(); + con.text_input.send_preedit_string(None, 0, 0); + con.text_input.send_done(); + } + FeedResult::Aborted if sym == syms::BackSpace => { + states.pop(); + if let Some(state) = states.last() { + send_preedit(state.char); + } else { + con.text_input.send_preedit_string(None, 0, 0); + } + con.text_input.send_done(); + } + FeedResult::Aborted => { + // nothing + } + FeedResult::Composed { string, keysym } => { + states.clear(); + let s = if string.is_some() { + string + } else if let Some(sym) = keysym + && let Some(char) = sym.char() + { + Some(char.encode_utf8(&mut buf) as &str) + } else { + None + }; + con.text_input.send_preedit_string(None, 0, 0); + con.text_input.send_commit_string(s); + con.text_input.send_done(); + } + } + } + forward_to_node + } + + fn on_modifiers(&self, _kb_state: &KeyboardState) -> bool { + true + } + + fn on_repeat_info(&self) { + // nothing + } +} diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_keyboard_grab_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_keyboard_grab_v2.rs index adedd3b3..53de655d 100644 --- a/src/ifs/wl_seat/text_input/zwp_input_method_keyboard_grab_v2.rs +++ b/src/ifs/wl_seat/text_input/zwp_input_method_keyboard_grab_v2.rs @@ -2,7 +2,10 @@ use { crate::{ backend::KeyState, client::{Client, ClientError}, - ifs::wl_seat::{text_input::zwp_input_method_v2::ZwpInputMethodV2, wl_keyboard}, + ifs::wl_seat::{ + text_input::{InputMethodKeyboardGrab, zwp_input_method_v2::ZwpInputMethodV2}, + wl_keyboard, + }, keyboard::{KeyboardState, KeyboardStateId}, leaks::Tracker, object::{Object, Version}, @@ -49,7 +52,7 @@ impl ZwpInputMethodKeyboardGrabV2 { self.kb_state_id.set(kb_state.id); } - pub fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) { + fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) { let serial = self.client.next_serial(); if self.kb_state_id.get() != kb_state.id { self.update_state(serial, kb_state); @@ -70,7 +73,7 @@ impl ZwpInputMethodKeyboardGrabV2 { }) } - pub fn on_modifiers(&self, kb_state: &KeyboardState) { + fn on_modifiers(&self, kb_state: &KeyboardState) { let serial = self.client.next_serial(); if self.kb_state_id.get() != kb_state.id { self.update_state(serial, kb_state); @@ -99,6 +102,22 @@ impl ZwpInputMethodKeyboardGrabV2 { } } +impl InputMethodKeyboardGrab for ZwpInputMethodKeyboardGrabV2 { + fn on_key(&self, time_usec: u64, key: u32, state: KeyState, kb_state: &KeyboardState) -> bool { + self.on_key(time_usec, key, state, kb_state); + false + } + + fn on_modifiers(&self, kb_state: &KeyboardState) -> bool { + self.on_modifiers(kb_state); + false + } + + fn on_repeat_info(&self) { + self.send_repeat_info(); + } +} + impl ZwpInputMethodKeyboardGrabV2RequestHandler for ZwpInputMethodKeyboardGrabV2 { type Error = ZwpInputMethodKeyboardGrabV2Error; diff --git a/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs b/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs index d78dbc7b..c848fb23 100644 --- a/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs +++ b/src/ifs/wl_seat/text_input/zwp_input_method_manager_v2.rs @@ -2,7 +2,7 @@ use { crate::{ client::{CAP_INPUT_METHOD, Client, ClientCaps, ClientError}, globals::{Global, GlobalName}, - ifs::wl_seat::text_input::{TextConnectReason, zwp_input_method_v2::ZwpInputMethodV2}, + ifs::wl_seat::text_input::zwp_input_method_v2::ZwpInputMethodV2, leaks::Tracker, object::{Object, Version}, wire::{ZwpInputMethodManagerV2Id, zwp_input_method_manager_v2::*}, @@ -72,7 +72,7 @@ impl ZwpInputMethodManagerV2RequestHandler for ZwpInputMethodManagerV2 { fn get_input_method(&self, req: GetInputMethod, _slf: &Rc) -> Result<(), Self::Error> { let seat = self.client.lookup(req.seat)?; - let inert = seat.global.input_method.is_some(); + let inert = seat.global.cannot_set_new_im(); let im = Rc::new(ZwpInputMethodV2 { id: req.input_method, client: self.client.clone(), @@ -90,9 +90,7 @@ impl ZwpInputMethodManagerV2RequestHandler for ZwpInputMethodManagerV2 { if inert { im.send_unavailable(); } else { - seat.global.input_method.set(Some(im)); - seat.global - .create_text_input_connection(TextConnectReason::InputMethodCreated); + seat.global.set_input_method(im); } Ok(()) } 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 3848bc56..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 @@ -5,7 +5,7 @@ use { wl_seat::{ WlSeatGlobal, text_input::{ - MAX_TEXT_SIZE, TextDisconnectReason, TextInputConnection, + InputMethod, MAX_TEXT_SIZE, TextDisconnectReason, TextInputConnection, zwp_input_method_keyboard_grab_v2::ZwpInputMethodKeyboardGrabV2, }, }, @@ -53,7 +53,7 @@ impl ZwpInputMethodV2 { } self.popups.clear(); if !self.inert { - self.seat.input_method.take(); + self.seat.remove_input_method(); } } @@ -104,6 +104,52 @@ impl ZwpInputMethodV2 { } } +impl InputMethod for ZwpInputMethodV2 { + fn set_connection(&self, con: Option<&Rc>) { + self.connection.set(con.cloned()); + } + + fn popups(&self) -> &SmallMap, 1> { + &self.popups + } + + fn activate(&self) { + self.activate(); + } + + fn deactivate(&self) { + self.send_deactivate(); + } + + fn content_type(&self, hint: u32, purpose: u32) { + self.send_content_type(hint, purpose); + } + + fn text_change_cause(&self, cause: u32) { + self.send_text_change_cause(cause); + } + + fn surrounding_text(&self, text: &str, cursor: u32, anchor: u32) { + self.send_surrounding_text(text, cursor, anchor); + } + + fn done(self: Rc, _seat: &WlSeatGlobal) { + (*self).send_done(); + } + + fn is_simple(&self) -> bool { + false + } + + fn cancel_simple(&self, _seat: &WlSeatGlobal) { + unreachable!(); + } + + fn enable_unicode_input(&self) { + // nothing + } +} + impl ZwpInputMethodV2RequestHandler for ZwpInputMethodV2 { type Error = ZwpInputMethodV2Error; diff --git a/src/ifs/wl_seat/text_input/zwp_text_input_v3.rs b/src/ifs/wl_seat/text_input/zwp_text_input_v3.rs index 54211b16..f831ac33 100644 --- a/src/ifs/wl_seat/text_input/zwp_text_input_v3.rs +++ b/src/ifs/wl_seat/text_input/zwp_text_input_v3.rs @@ -5,8 +5,8 @@ use { wl_seat::{ WlSeatGlobal, text_input::{ - MAX_TEXT_SIZE, TextConnectReason, TextDisconnectReason, TextInputConnection, - zwp_input_method_v2::ZwpInputMethodV2, + InputMethod, MAX_TEXT_SIZE, TextConnectReason, TextDisconnectReason, + TextInputConnection, }, }, wl_surface::WlSurface, @@ -72,13 +72,13 @@ impl ZwpTextInputV3 { } } - pub fn send_all_to(&self, im: &ZwpInputMethodV2) { + pub fn send_all_to(&self, im: &dyn InputMethod) { let state = &*self.state.borrow(); { let (a, b, c) = &state.surrounding_text; - im.send_surrounding_text(a, *b, *c); + im.surrounding_text(a, *b, *c); } - im.send_content_type(state.content_type.0, state.content_type.1); + im.content_type(state.content_type.0, state.content_type.1); } pub fn send_enter(&self, surface: &WlSurface) { @@ -255,7 +255,7 @@ impl ZwpTextInputV3RequestHandler for ZwpTextInputV3 { if state.cursor_rectangle != val && let Some(con) = &con { - for (_, popup) in &con.input_method.popups { + for (_, popup) in con.input_method.popups() { popup.schedule_positioning(); } } @@ -264,26 +264,26 @@ impl ZwpTextInputV3RequestHandler for ZwpTextInputV3 { if let Some(val) = pending.content_type { if let Some(con) = &con { sent_any = true; - con.input_method.send_content_type(val.0, val.1); + con.input_method.content_type(val.0, val.1); } state.content_type = val; } if let Some(val) = pending.text_change_cause { if let Some(con) = &con { sent_any = true; - con.input_method.send_text_change_cause(val); + con.input_method.text_change_cause(val); } state.text_change_cause = val; } if let Some(val) = pending.surrounding_text { if let Some(con) = &con { sent_any = true; - con.input_method.send_surrounding_text(&val.0, val.1, val.2); + con.input_method.surrounding_text(&val.0, val.1, val.2); } state.surrounding_text = val; } if sent_any && let Some(con) = &con { - con.input_method.send_done(); + con.input_method.clone().done(&self.seat); } Ok(()) } diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 282a7696..a108b7f4 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -784,7 +784,7 @@ impl WlSurface { } } for (_, con) in &self.text_input_connections { - for (_, popup) in &con.input_method.popups { + for (_, popup) in con.input_method.popups() { popup.schedule_positioning(); } } diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 82f013ba..7a66b935 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -335,7 +335,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(21), + version: s.jay_compositor.1.min(22), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 6d240d96..d77948d4 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -85,6 +85,10 @@ pub enum SimpleCommand { CreateMark, JumpToMark, PopMode(bool), + EnableSimpleIm(bool), + ToggleSimpleImEnabled, + ReloadSimpleIm, + EnableUnicodeInput, } #[derive(Debug, Clone)] @@ -446,6 +450,11 @@ pub struct Vrr { pub cursor_hz: Option, } +#[derive(Debug, Clone)] +pub struct SimpleIm { + pub enabled: Option, +} + #[derive(Debug, Clone)] pub struct Xwayland { pub scaling_mode: Option, @@ -520,6 +529,7 @@ pub struct Config { pub middle_click_paste: Option, pub input_modes: AHashMap, pub workspace_display_order: Option, + pub simple_im: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 46ec09f2..c786c71d 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -39,6 +39,7 @@ mod output; mod output_match; mod repeat_rate; pub mod shortcuts; +mod simple_im; mod status; mod tearing; mod theme; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 10a2efcd..9063fc89 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -155,6 +155,11 @@ impl ActionParser<'_> { "jump-to-mark" => JumpToMark, "clear-modes" => PopMode(false), "pop-mode" => PopMode(true), + "enable-simple-im" => EnableSimpleIm(true), + "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/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 59268800..03cefe0f 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -30,6 +30,7 @@ use { ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError, parse_modified_keysym_str, }, + simple_im::SimpleImParser, status::StatusParser, tearing::TearingParser, theme::ThemeParser, @@ -139,7 +140,13 @@ impl Parser for ConfigParser<'_> { show_bar, focus_history_val, ), - (middle_click_paste, input_modes_val, workspace_display_order_val, auto_reload), + ( + middle_click_paste, + input_modes_val, + workspace_display_order_val, + auto_reload, + simple_im_val, + ), ) = ext.extract(( ( opt(val("keymap")), @@ -194,6 +201,7 @@ impl Parser for ConfigParser<'_> { opt(val("modes")), opt(val("workspace-display-order")), recover(opt(bol("auto-reload"))), + opt(val("simple-im")), ), ))?; let mut keymap = None; @@ -505,6 +513,15 @@ impl Parser for ConfigParser<'_> { } } } + let mut simple_im = None; + if let Some(value) = simple_im_val { + match value.parse(&mut SimpleImParser(self.0)) { + Ok(v) => simple_im = Some(v), + Err(e) => { + log::warn!("Could not parse simple IM setting: {}", self.0.error(e)); + } + } + } Ok(Config { keymap, repeat_rate, @@ -549,6 +566,7 @@ impl Parser for ConfigParser<'_> { middle_click_paste: middle_click_paste.despan(), input_modes, workspace_display_order, + simple_im, }) } } diff --git a/toml-config/src/config/parsers/simple_im.rs b/toml-config/src/config/parsers/simple_im.rs new file mode 100644 index 00000000..b4c08095 --- /dev/null +++ b/toml-config/src/config/parsers/simple_im.rs @@ -0,0 +1,44 @@ +use { + crate::{ + config::{ + SimpleIm, + context::Context, + extractor::{Extractor, ExtractorError, bol, opt, recover}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum SimpleImParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), +} + +pub struct SimpleImParser<'a>(pub &'a Context<'a>); + +impl Parser for SimpleImParser<'_> { + type Value = SimpleIm; + type Error = SimpleImParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (enabled,) = ext.extract((recover(opt(bol("enabled"))),))?; + Ok(SimpleIm { + enabled: enabled.despan(), + }) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index cf5bd21b..b4d99fdb 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -224,6 +224,22 @@ impl Action { let state = state.clone(); b.new(move || state.pop_mode(pop)) } + SimpleCommand::EnableSimpleIm(v) => { + let persistent = state.persistent.clone(); + b.new(move || persistent.seat.set_simple_im_enabled(v)) + } + SimpleCommand::ToggleSimpleImEnabled => { + let persistent = state.persistent.clone(); + b.new(move || persistent.seat.toggle_simple_im_enabled()) + } + SimpleCommand::ReloadSimpleIm => { + 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(); @@ -1559,6 +1575,11 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 0de5ae68..6f5df501 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -361,7 +361,7 @@ ] }, { - "description": "Sets the log level of the compositor..\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-j = { type = \"set-log-level\", level = \"debug\" }\n ```\n", + "description": "Sets the log level of the compositor.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-j = { type = \"set-log-level\", level = \"debug\" }\n ```\n", "type": "object", "properties": { "type": { @@ -1049,6 +1049,10 @@ "workspace-display-order": { "description": "Configures the order of workspaces displayed.\n\nThe default is `manual`.\n\n- Example:\n\n ```toml\n workspace-display-order = \"sorted\"\n ```\n", "$ref": "#/$defs/WorkspaceDisplayOrder" + }, + "simple-im": { + "description": "Configures the simple, XCompose based input method.\n\nBy default, the input method is enabled. \n\nEven if the input method is enabled, it will only be used if there is no\nrunning external IM.\n\n- Example:\n\n ```toml\n [simple-im]\n enabled = false\n ```\n", + "$ref": "#/$defs/SimpleIm" } }, "required": [] @@ -1835,9 +1839,25 @@ "create-mark", "jump-to-mark", "clear-modes", - "pop-mode" + "pop-mode", + "enable-simple-im", + "disable-simple-im", + "toggle-simple-im-enabled", + "reload-simple-im", + "enable-unicode-input" ] }, + "SimpleIm": { + "description": "Describes the settings of the simple, XCompose based input method.\n\n- Example:\n\n ```toml\n [simple-im]\n enabled = false\n ```\n", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether the input method is enabled.\n\nEven if the input method is enabled, it will only be used if there is no\nrunning external IM.\n" + } + }, + "required": [] + }, "Status": { "description": "The configuration of a status program whose output will be shown in the bar.\n\n- Example:\n\n ```toml\n [status]\n format = \"i3bar\"\n exec = \"i3status\"\n ```\n", "type": "object", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 41a72d2e..43a32c02 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -538,7 +538,7 @@ This table is a tagged union. The variant is determined by the `type` field. It - `set-log-level`: - Sets the log level of the compositor.. + Sets the log level of the compositor. - Example: @@ -2145,6 +2145,24 @@ The table has the following fields: The value of this field should be a [WorkspaceDisplayOrder](#types-WorkspaceDisplayOrder). +- `simple-im` (optional): + + Configures the simple, XCompose based input method. + + By default, the input method is enabled. + + Even if the input method is enabled, it will only be used if there is no + running external IM. + + - Example: + + ```toml + [simple-im] + enabled = false + ``` + + The value of this field should be a [SimpleIm](#types-SimpleIm). + ### `Connector` @@ -4177,6 +4195,59 @@ The string should have one of the following values: Pops the topmost mode from the input-mode stack. +- `enable-simple-im`: + + Enables the simple, XCompose based input method. + + Even if the input method is enabled, it will only be used if there is no + running external IM. + +- `disable-simple-im`: + + Disables the simple, XCompose based input method. + +- `toggle-simple-im-enabled`: + + Toggles whether the simple, XCompose based input method is enabled. + +- `reload-simple-im`: + + Reloads the simple, XCompose based input method. + + 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. + + + + +### `SimpleIm` + +Describes the settings of the simple, XCompose based input method. + +- Example: + + ```toml + [simple-im] + enabled = false + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `enabled` (optional): + + Whether the input method is enabled. + + Even if the input method is enabled, it will only be used if there is no + running external IM. + + The value of this field should be a boolean. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 8932c35d..32d122bc 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -497,7 +497,7 @@ Action: ref: Theme set-log-level: description: | - Sets the log level of the compositor.. + Sets the log level of the compositor. - Example: @@ -1033,6 +1033,28 @@ SimpleActionName: description: Disables all previously set input modes, clearing the input-mode stack. - value: pop-mode description: Pops the topmost mode from the input-mode stack. + - value: enable-simple-im + description: | + Enables the simple, XCompose based input method. + + Even if the input method is enabled, it will only be used if there is no + running external IM. + - value: disable-simple-im + description: | + Disables the simple, XCompose based input method. + - value: toggle-simple-im-enabled + description: | + Toggles whether the simple, XCompose based input method is enabled. + - value: reload-simple-im + description: | + 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: @@ -2881,6 +2903,23 @@ Config: ```toml workspace-display-order = "sorted" ``` + simple-im: + ref: SimpleIm + required: false + description: | + Configures the simple, XCompose based input method. + + By default, the input method is enabled. + + Even if the input method is enabled, it will only be used if there is no + running external IM. + + - Example: + + ```toml + [simple-im] + enabled = false + ``` Idle: @@ -4195,3 +4234,25 @@ ClientCapabilities: description: An array of masks that are OR'd. items: ref: ClientCapabilities + + +SimpleIm: + kind: table + description: | + Describes the settings of the simple, XCompose based input method. + + - Example: + + ```toml + [simple-im] + enabled = false + ``` + fields: + enabled: + kind: boolean + required: false + description: | + Whether the input method is enabled. + + Even if the input method is enabled, it will only be used if there is no + running external IM. diff --git a/wire/jay_input.txt b/wire/jay_input.txt index f94b95ce..2f81bb75 100644 --- a/wire/jay_input.txt +++ b/wire/jay_input.txt @@ -134,6 +134,15 @@ request set_middle_button_emulation (since = 19) { enabled: u32, } +request set_simple_im_enabled (since = 22) { + seat: str, + enabled: u32, +} + +request reload_simple_im (since = 22) { + seat: str, +} + # events event seat {