Skip to content

Commit 25af40e

Browse files
committed
add macos forward-to-ime-modifier-mask
- gate interpretKeyEvents: forwarding by a configurable modifier mask - handle IME direct commits without preedit (e.g. SKK kana mode) - swallow keys consumed via IMKTextInput.selectMode (SKK Ctrl+J, q)
1 parent 97340fe commit 25af40e

10 files changed

Lines changed: 227 additions & 11 deletions

File tree

docs/docs/config.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,12 +879,20 @@ ignore-selection-foreground-color = false
879879
- Automatically updates position when cursor moves via keyboard, mouse, or any other method
880880
- Set to `false` to use system default IME positioning behavior
881881

882+
- `forward-to-ime-modifier-mask` - Modifier mask deciding when a key event is forwarded to the macOS IME (default: `["shift", "ctrl", "alt", "super"]`)
883+
- macOS only. Has no effect on other platforms.
884+
- A key event is forwarded to the IME (`interpretKeyEvents:`) when no modifier is pressed, or when the pressed modifiers intersect this mask. Otherwise the event is handled directly by the application without going through the IME.
885+
- Useful for input methods such as SKK that need to receive `Ctrl`+key combinations directly. Set to `["shift", "ctrl"]` to keep `Ctrl-J` (and similar conversion shortcuts) reaching the IME while still letting unmodified keys pass through.
886+
- Accepted values (case-insensitive): `shift`, `ctrl` (alias `control`), `alt` (alias `option`), `super` (aliases `cmd`, `command`). Unknown values are ignored with a warning.
887+
- The default keeps the historical behavior where every key event reaches the IME.
888+
882889
Example:
883890

884891
```toml
885892
[keyboard]
886893
disable-ctlseqs-alt = false
887894
ime-cursor-positioning = true
895+
forward-to-ime-modifier-mask = ["shift", "ctrl"]
888896
```
889897

890898
## line-height

docs/docs/features/ime-support.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,45 @@ ime-cursor-positioning = true # Enable IME cursor positioning (default)
5050
- `true` (default): IME popups appear at the cursor position
5151
- `false`: Use system default IME positioning behavior
5252

53+
## Forwarding Modifier Keys to the IME (macOS)
54+
55+
On macOS, Rio decides per-keypress whether to forward the event to the active
56+
input method (`interpretKeyEvents:`) or handle it directly. The
57+
`forward-to-ime-modifier-mask` option controls this decision.
58+
59+
```toml
60+
[keyboard]
61+
# Default: every modifier combination is forwarded to the IME (historical behavior).
62+
forward-to-ime-modifier-mask = ["shift", "ctrl", "alt", "super"]
63+
```
64+
65+
A key event is forwarded to the IME when no modifier is pressed, OR when the
66+
pressed modifiers intersect this mask. Otherwise the event is handled directly
67+
by Rio without going through the IME.
68+
69+
### Use Case: SKK
70+
71+
SKK and similar input methods rely on `Ctrl`+key combinations (e.g. `Ctrl-J` to
72+
toggle kana input) and `Shift`+letter for conversion. Keep these combinations
73+
reaching the IME while letting other modifiers (such as `Cmd`+key shortcuts)
74+
bypass it:
75+
76+
```toml
77+
[keyboard]
78+
forward-to-ime-modifier-mask = ["shift", "ctrl"]
79+
```
80+
81+
### Accepted Values
82+
83+
Case-insensitive. Unknown entries are ignored with a warning.
84+
85+
| Value | Modifier |
86+
|-------|----------|
87+
| `shift` | Shift |
88+
| `ctrl` (or `control`) | Control |
89+
| `alt` (or `option`) | Option / Alt |
90+
| `super` (or `cmd`, `command`) | Command |
91+
5392
## Benefits
5493

5594
### Enhanced User Experience

frontends/rioterm/src/bindings/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ pub fn default_key_bindings(config: &rio_backend::config::Config) -> Vec<KeyBind
731731
bindings.extend(platform_key_bindings(
732732
config.navigation.has_navigation_key_bindings(),
733733
config.navigation.use_split,
734-
config.keyboard,
734+
config.keyboard.clone(),
735735
));
736736

737737
// Add hint bindings

frontends/rioterm/src/router/window.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ pub fn configure_window(winit_window: &Window, config: &Config) {
187187
// OnlyRight - The right `Option` key is treated as `Alt`.
188188
// Both - Both `Option` keys are treated as `Alt`.
189189
// None - No special handling is applied for `Option` key.
190+
use rio_window::keyboard::ModifiersState;
190191
use rio_window::platform::macos::{OptionAsAlt, WindowExtMacOS};
191192

192193
match config.option_as_alt.to_lowercase().as_str() {
@@ -195,6 +196,20 @@ pub fn configure_window(winit_window: &Window, config: &Config) {
195196
"right" => winit_window.set_option_as_alt(OptionAsAlt::OnlyRight),
196197
_ => {}
197198
}
199+
200+
let mut mask = ModifiersState::empty();
201+
for name in &config.keyboard.forward_to_ime_modifier_mask {
202+
match name.to_lowercase().as_str() {
203+
"shift" => mask |= ModifiersState::SHIFT,
204+
"ctrl" | "control" => mask |= ModifiersState::CONTROL,
205+
"alt" | "option" => mask |= ModifiersState::ALT,
206+
"super" | "cmd" | "command" => mask |= ModifiersState::SUPER,
207+
other => tracing::warn!(
208+
"ignoring unknown forward-to-ime-modifier-mask value: {other:?}"
209+
),
210+
}
211+
}
212+
winit_window.set_forward_to_ime_modifier_mask(mask);
198213
}
199214

200215
let is_transparent = config.window.opacity < 1.;

frontends/rioterm/src/screen/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ impl Screen<'_> {
252252
split_active_color: config.colors.split_active,
253253
panel: config.panel,
254254
title: config.title.clone(),
255-
keyboard: config.keyboard,
255+
keyboard: config.keyboard.clone(),
256256
scrollback_history_limit: config.scrollback_history_limit,
257257
};
258258

@@ -537,7 +537,7 @@ impl Screen<'_> {
537537
.set_multiplier_and_divider(config.scroll.multiplier, config.scroll.divider);
538538

539539
// Update keyboard config in context manager
540-
self.context_manager.config.keyboard = config.keyboard;
540+
self.context_manager.config.keyboard = config.keyboard.clone();
541541

542542
self.sugarloaf
543543
.set_background_color(Some(self.renderer.dynamic_background.1));

rio-backend/src/config/defaults.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ pub fn default_ime_cursor_positioning() -> bool {
148148
true
149149
}
150150

151+
#[inline]
152+
pub fn default_forward_to_ime_modifier_mask() -> Vec<String> {
153+
vec![
154+
String::from("shift"),
155+
String::from("ctrl"),
156+
String::from("alt"),
157+
String::from("super"),
158+
]
159+
}
160+
151161
pub fn default_config_file_content() -> String {
152162
String::from(
153163
"# See the full configuration reference: https://rioterm.com/docs/config\n",

rio-backend/src/config/keyboard.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use serde::{Deserialize, Serialize};
22

3-
use super::defaults::{default_disable_ctlseqs_alt, default_ime_cursor_positioning};
3+
use super::defaults::{
4+
default_disable_ctlseqs_alt, default_forward_to_ime_modifier_mask,
5+
default_ime_cursor_positioning,
6+
};
47

5-
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
8+
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
69
pub struct Keyboard {
710
// Disable ctlseqs with ALT keys
811
// For example: Terminal.app does not deal with ctlseqs with ALT keys
@@ -19,6 +22,20 @@ pub struct Keyboard {
1922
rename = "ime-cursor-positioning"
2023
)]
2124
pub ime_cursor_positioning: bool,
25+
26+
// Modifier mask deciding when a key event is forwarded to the macOS IME.
27+
// A key event is forwarded when no modifier is pressed, or when the
28+
// pressed modifiers intersect this mask. Otherwise the event is handled
29+
// directly by the application without going through the IME.
30+
//
31+
// Accepted values (case-insensitive): "shift", "ctrl", "alt", "super".
32+
// Useful for input methods like SKK that need to receive Ctrl+key
33+
// combinations directly.
34+
#[serde(
35+
default = "default_forward_to_ime_modifier_mask",
36+
rename = "forward-to-ime-modifier-mask"
37+
)]
38+
pub forward_to_ime_modifier_mask: Vec<String>,
2239
}
2340

2441
#[allow(clippy::derivable_impls)]
@@ -30,6 +47,7 @@ impl Default for Keyboard {
3047
#[cfg(not(target_os = "macos"))]
3148
disable_ctlseqs_alt: false,
3249
ime_cursor_positioning: default_ime_cursor_positioning(),
50+
forward_to_ime_modifier_mask: default_forward_to_ime_modifier_mask(),
3351
}
3452
}
3553
}

rio-window/src/platform/macos.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use std::os::raw::c_void;
1818

1919
use crate::event_loop::{ActiveEventLoop, EventLoopBuilder};
20+
use crate::keyboard::ModifiersState;
2021
use crate::monitor::MonitorHandle;
2122
use crate::window::{Window, WindowAttributes};
2223

@@ -106,6 +107,21 @@ pub trait WindowExtMacOS {
106107
/// Getter for the [`WindowExtMacOS::set_option_as_alt`].
107108
fn option_as_alt(&self) -> OptionAsAlt;
108109

110+
/// Set which modifier keys cause a key event to be forwarded to the macOS
111+
/// IME (`interpretKeyEvents:`).
112+
///
113+
/// A key event is forwarded to the IME when no modifier is pressed, OR
114+
/// when the pressed modifiers intersect this mask. Otherwise the event is
115+
/// handled directly by the application without going through the IME.
116+
///
117+
/// This is useful for input methods (e.g. SKK) that need to receive
118+
/// `Ctrl`/`Shift` combinations directly instead of having them processed
119+
/// as terminal control sequences.
120+
fn set_forward_to_ime_modifier_mask(&self, mask: ModifiersState);
121+
122+
/// Getter for the [`WindowExtMacOS::set_forward_to_ime_modifier_mask`].
123+
fn forward_to_ime_modifier_mask(&self) -> ModifiersState;
124+
109125
/// Makes the titlebar bigger, effectively adding more space around the
110126
/// window controls if the titlebar is invisible.
111127
fn set_unified_titlebar(&self, unified_titlebar: bool);
@@ -204,6 +220,18 @@ impl WindowExtMacOS for Window {
204220
self.window.maybe_wait_on_main(|w| w.option_as_alt())
205221
}
206222

223+
#[inline]
224+
fn set_forward_to_ime_modifier_mask(&self, mask: ModifiersState) {
225+
self.window
226+
.maybe_queue_on_main(move |w| w.set_forward_to_ime_modifier_mask(mask))
227+
}
228+
229+
#[inline]
230+
fn forward_to_ime_modifier_mask(&self) -> ModifiersState {
231+
self.window
232+
.maybe_wait_on_main(|w| w.forward_to_ime_modifier_mask())
233+
}
234+
207235
#[inline]
208236
fn set_unified_titlebar(&self, unified_titlebar: bool) {
209237
self.window

0 commit comments

Comments
 (0)