From 1b6dbb2fe72a86a840ef88fc837a5bb0aae972f2 Mon Sep 17 00:00:00 2001 From: momo <22584870+nz3u@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:04:29 +0800 Subject: [PATCH] mpv chat: support IME for CJK input via input-ime The console chat input only relied on key bindings, which deliver single US-ASCII-representable keys and therefore can never receive text composed by a system input method. Bind the 'any_unicode' text input event to feed composed UTF-8 strings into the input line, and toggle mpv's 'input-ime' property while the input box is open so an IME can compose Chinese/Japanese/Korean text. Both 'any_unicode' and 'input-ime' are available since mpv 0.40.0; on older builds the existing per-key bindings are used unchanged. --- syncplay/resources/syncplayintf.lua | 129 +++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/syncplay/resources/syncplayintf.lua b/syncplay/resources/syncplayintf.lua index a6bdc42e..466ef535 100644 --- a/syncplay/resources/syncplayintf.lua +++ b/syncplay/resources/syncplayintf.lua @@ -50,6 +50,13 @@ local FONT_SIZE_MULTIPLIER = 2 local chat_log = {} +-- First minor version of mpv exposing both the 'any_unicode' text-input +-- binding and the runtime 'input-ime' property (both landed in 0.40.0). +local IME_TEXT_INPUT_SUPPORTED_VERSION = 40 +local ime_supported = false +local ime_enabled_before_repl = false +local ime_active = false + local assdraw = require "mp.assdraw" local opt = require 'mp.options' @@ -570,9 +577,15 @@ function set_active(active) repl_active = true insert_mode = false mp.enable_key_bindings('repl-input', 'allow-hide-cursor+allow-vo-dragging') + define_text_input_binding() + enable_ime_for_input() else repl_active = false mp.disable_key_bindings('repl-input') + -- Drop the text binding first so keys following the close are not + -- swallowed by it; then hand the IME back to mpv's normal key mode. + remove_text_input_binding() + disable_ime_after_input() end if default_oscvisibility_state ~= "never" and opts['OscVisibilityChangeCompatible'] == true then if active then @@ -749,7 +762,8 @@ function trim_input() return end --- Insert a character at the current cursor position (' '-'~', Shift+Enter) +-- Insert a character (possibly a multi-byte UTF-8 sequence, e.g. produced by an +-- IME) at the current cursor position (' '-'~', Shift+Enter, IME composition) function handle_char_input(c) if c == nil then return end if c == "\\" then c = opts['backslashSubstituteCharacter'] end @@ -767,6 +781,119 @@ function handle_char_input(c) update() end +-- Receive composed text from mpv's text input path (`any_unicode` binding). +-- This is what makes system IME candidates (CJK etc.) reach the input line; +-- key bindings alone only ever deliver single US-ASCII-representable keys. +function handle_text_input(info) + if info == nil or info.canceled then return end + -- Only deliver text while the input box is actually open, so stray keys + -- keep going to mpv's own bindings instead of opening the chat input. + if not repl_active then return end + local event = info.event + if event ~= "press" and event ~= "down" and event ~= "repeat" then return end + local text = info.key_text + if text == nil or text == "" then return end + local maximum = opts['MaxChatMessageLength'] + -- Feed characters one at a time so the length cap stays enforced on the + -- whole composed string, not just on the individual pieces. + while text ~= "" and maximum ~= nil and line:len() + text:len() > maximum do + local char_end = next_utf8(text, 0) + local character = text:sub(1, char_end - 1) + if character == "" then return end + text = text:sub(char_end) + insert_char_at_cursor(character) + end + if text ~= "" then + insert_char_at_cursor(text) + end +end + +function insert_char_at_cursor(c) + if c == nil or c == "" then return end + if c == "\\" then c = opts['backslashSubstituteCharacter'] end + if key_hints_enabled and (string.len(line) > 0 or opts['chatDirectInput'] == false) then + key_hints_enabled = false + end + if insert_mode then + line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor)) + else + line = line:sub(1, cursor - 1) .. c .. line:sub(cursor) + end + cursor = cursor + c:len() + trim_input() + update() +end + +-- Accessibility for the input box: let the system input method compose text +-- while text is being entered, then put mpv back into key-driven mode so the +-- remaining key bindings work normally again. +function enable_ime_for_input() + save_ime_state() + set_ime_enabled(true) +end + +function disable_ime_after_input() + if not ime_supported then return end + set_ime_enabled(false) + ime_enabled_before_repl = false +end + +-- Register the 'any_unicode' binding that delivers composed text (including +-- IME candidates) to the input line. Only available on mpv 0.40.0+. +local has_text_input_binding = false +function define_text_input_binding() + if has_text_input_binding or not ime_supported then return end + mp.add_forced_key_binding( + "any_unicode", "__syncplayintf_text", handle_text_input, + { repeatable = true, complex = true }) + has_text_input_binding = true +end + +function remove_text_input_binding() + if not has_text_input_binding then return end + mp.remove_key_binding("__syncplayintf_text") + has_text_input_binding = false +end + +-- Toggle the system input method so composed (e.g. Chinese/Japanese) text can +-- be entered while the input box is open, and restore the previous state after. +-- Every entry point into text input routes through here so the flag cannot leak. +function set_ime_enabled(enabled) + if not ime_supported then return end + local wanted = enabled == true + if wanted == ime_active then return end + local ok, failure = pcall(function() + mp.set_property_bool("input-ime", wanted) + end) + if not ok or failure == false then return end + ime_active = wanted +end + +function save_ime_state() + if not ime_supported then return end + local ok, previous = pcall(function() + return mp.get_property_bool("input-ime", false) + end) + if not ok then return end + ime_enabled_before_repl = previous == true +end + +function detect_ime_support() + -- ANY_UNICODE bindings and the input-ime property arrived together in + -- mpv 0.40.0; on older builds fall back to the per-key bindings only. + local version = tostring(mp.get_property_native("mpv-version", "") or "") + local major, minor = string.match(version, "v?(%d+)%.(%d+)") + major, minor = tonumber(major), tonumber(minor) + if major == nil or minor == nil then + return false + end + if major ~= 0 then + return true + end + return minor >= IME_TEXT_INPUT_SUPPORTED_VERSION +end +ime_supported = detect_ime_support() + -- Remove the character behind the cursor (Backspace) function handle_backspace() if cursor <= 1 then return end