macos: fix dictation support, with new configs for preview text (#7453)

closes: #4592 & #5180
This commit is contained in:
Phil Gore 2026-09-15 05:51:23 -06:00 committed by GitHub
parent 016b96272b
commit 2658f629cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 22 deletions

View File

@ -52,6 +52,8 @@
<string>An application launched via WezTerm would like to access your location information while active.</string>
<key>NSMicrophoneUsageDescription</key>
<string>An application launched via WezTerm would like to access your microphone.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>An application launched via WezTerm would like to use speech recognition.</string>
<key>NSRemindersUsageDescription</key>
<string>An application launched via WezTerm would like to access your reminders.</string>
<key>NSDocumentsFolderUsageDescription</key>

View File

@ -160,6 +160,10 @@ pub struct Palette {
pub visual_bell: Option<RgbaColor>,
/// The color to use for the cursor when a dead key or leader state is active
pub compose_cursor: Option<RgbaColor>,
/// The foreground color for composition/IME preview text (e.g., dictation)
pub compose_fg: Option<RgbaColor>,
/// The background color for composition/IME preview text (e.g., dictation)
pub compose_bg: Option<RgbaColor>,
pub copy_mode_active_highlight_fg: Option<ColorSpec>,
pub copy_mode_active_highlight_bg: Option<ColorSpec>,
@ -217,6 +221,8 @@ impl Palette {
split: overlay!(split),
visual_bell: overlay!(visual_bell),
compose_cursor: overlay!(compose_cursor),
compose_fg: overlay!(compose_fg),
compose_bg: overlay!(compose_bg),
copy_mode_active_highlight_fg: overlay!(copy_mode_active_highlight_fg),
copy_mode_active_highlight_bg: overlay!(copy_mode_active_highlight_bg),
copy_mode_inactive_highlight_fg: overlay!(copy_mode_inactive_highlight_fg),

View File

@ -112,6 +112,15 @@ config.colors = {
-- to this color to give a visual cue about the compose state.
compose_cursor = 'orange',
-- Override the foreground color for composition/IME preview text,
-- such as text being dictated via macOS dictation or composed via
-- a dead key sequence. Falls back to the cursor foreground color.
compose_fg = '#1e1e2e', -- {{since('nightly', inline=True)}}
-- Override the background color for composition/IME preview text.
-- Falls back to the cursor background color.
compose_bg = '#f5c2e7', -- {{since('nightly', inline=True)}}
-- Colors for copy_mode and quick_select
-- available since: 20220807-113146-c2fee766
-- In copy_mode, the color of the active text is:

View File

@ -570,11 +570,32 @@ impl crate::TermWindow {
self.dead_key_status != DeadKeyStatus::None || self.leader_is_active();
if dead_key_or_leader && params.is_active_pane {
let (fg_color, bg_color) = if self.use_reverse_video_cursor(&params) {
(params.bg_color, params.fg_color)
} else {
(params.cursor_fg, params.cursor_bg)
};
let (fg_color, bg_color) = (
params
.config
.resolved_palette
.compose_fg
.map(|c| c.to_linear())
.unwrap_or_else(|| {
if self.use_reverse_video_cursor(&params) {
params.bg_color
} else {
params.cursor_fg
}
}),
params
.config
.resolved_palette
.compose_bg
.map(|c| c.to_linear())
.unwrap_or_else(|| {
if self.use_reverse_video_cursor(&params) {
params.fg_color
} else {
params.cursor_bg
}
}),
);
let fg_color = self.ensure_min_contrast(fg_color, bg_color);

View File

@ -303,6 +303,23 @@ impl crate::TermWindow {
0.0..0.0
};
// Render composition/IME preview background
if composition_width > 0 {
if let Some(compose_bg) = &params.config.resolved_palette.compose_bg {
let start = params.left_pixel_x + (params.cursor.x as f32 * cell_width);
let width = composition_width as f32 * cell_width;
let mut quad = self
.filled_rectangle(
layers,
0,
euclid::rect(start, params.top_pixel_y, width, cell_height),
compose_bg.to_linear(),
)
.context("filled_rectangle")?;
quad.set_hsv(hsv);
}
}
// Consider cursor
if !cursor_range.is_empty() {
let (fg_color, bg_color) = if let Some(c) = &cursor_cell {
@ -736,7 +753,14 @@ impl crate::TermWindow {
// Create an updated line with the composition overlaid
let mut line = params.line.clone();
let seqno = line.current_seqno();
line.overlay_text_with_attribute(*cursor_x, &composing, CellAttributes::blank(), seqno);
let mut compose_attrs = CellAttributes::blank();
if let Some(fg) = &params.config.resolved_palette.compose_fg {
compose_attrs
.set_foreground(ColorAttribute::TrueColorWithDefaultFallback((*fg).into()));
}
line.overlay_text_with_attribute(*cursor_x, &composing, compose_attrs, seqno);
line.cluster(bidi_hint)
} else {
params.line.cluster(bidi_hint)

View File

@ -2050,7 +2050,15 @@ impl WindowView {
}
extern "C" fn selected_range(_this: &mut Object, _sel: Sel) -> NSRange {
NSRange::new(NSNotFound as _, 0)
// Return an empty range at position 0 rather than {NSNotFound, 0}.
// NSTextInputClient expects character indices into a text storage
// buffer, but as a terminal emulator we don't maintain one—input
// is handled through insert_text/set_marked_text callbacks instead.
// Returning NSNotFound causes macOS dictation to silently bail out
// because it has no valid insertion point. An empty selection at 0
// satisfies the protocol and enables dictation. This is the same
// workaround used by kitty and Emacs. See #4592
NSRange::new(0, 0)
}
// Called by the IME when inserting composed text and/or emoji
@ -2110,22 +2118,16 @@ impl WindowView {
let mut inner = myself.inner.borrow_mut();
inner.ime_text = s.to_string();
/*
let key_is_down = inner.key_is_down.take().unwrap_or(true);
// Show composition preview for dictation; see #4592
let status = if s.is_empty() {
DeadKeyStatus::None
} else {
DeadKeyStatus::Composing(s.to_string())
};
inner
.events
.dispatch(WindowEvent::AdviseDeadKeyStatus(status));
let key = KeyCode::composed(s);
let event = KeyEvent {
key,
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down,
}
.normalize_shift();
inner.ime_last_event.replace(event.clone());
inner.events.dispatch(WindowEvent::KeyEvent(event));
*/
inner.ime_last_event.take();
inner.ime_state = ImeDisposition::Acted;
}
@ -2141,6 +2143,9 @@ impl WindowView {
inner.ime_text.clear();
inner.ime_last_event.take();
inner.ime_state = ImeDisposition::Acted;
inner
.events
.dispatch(WindowEvent::AdviseDeadKeyStatus(DeadKeyStatus::None));
}
}