fix(text-injection): detect KWin 6 VirtualKeyboard property (#919)
* fix(text-injection): detect KWin 6 VirtualKeyboard property On KDE Plasma 6 the org.kde.kwin.VirtualKeyboard 'enabled' property no longer exists; the setting is reported as 'available'. Query 'available' first, keep 'enabled' as the Plasma 5 fallback, and accept both the real (<true>,) gdbus output and the doubly wrapped (<<true>>,) form. With the bridge check working, KDE sessions with Virtual Keyboard set to "IBus Wayland" can safely use scoped IBus injection again: the leftover-daemon trap of #752 is exactly what a confirmed VK bridge disambiguates. KDE without a confirmed VK stays excluded. KWin's RemoteDesktop portal scrambles letter case, so in auto mode on KDE sessions an installed ydotool is preferred over the portal -- the ordering used before the portal backend existed. Fixes #911 * fix(text-injection): keep KDE portal fallback ahead of daemonless ydotool On KDE, when ydotool is installed but cannot run (no daemon, no /dev/uinput access) and wtype is absent, the daemonless-ydotool branch fired before the portal fallback -- selecting a backend that fails at injection time while a working portal sat unused. The portal fallback now precedes the daemonless branch; working ydotool still outranks the portal on KDE. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
co-authored by
Jatin K Malik
parent
9eba80b53d
commit
622d0e054d
@@ -314,52 +314,62 @@ class TextInjector:
|
||||
"""Return True when KWin VirtualKeyboard / input method is enabled.
|
||||
|
||||
On KDE Plasma Wayland, IBus only reaches native apps when this is on
|
||||
(issue #574). Disabled or unqueryable → treat IBus as unbridged.
|
||||
(issue #574). KWin 6 reports the setting through the ``available``
|
||||
property; ``enabled`` existed on Plasma 5 and is kept as a fallback
|
||||
(issue #911). Disabled or unqueryable → treat IBus as unbridged.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gdbus",
|
||||
"call",
|
||||
"--session",
|
||||
"--dest",
|
||||
"org.kde.KWin",
|
||||
"--object-path",
|
||||
"/VirtualKeyboard",
|
||||
"--method",
|
||||
"org.freedesktop.DBus.Properties.Get",
|
||||
"org.kde.kwin.VirtualKeyboard",
|
||||
"enabled",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2,
|
||||
env=host_env(),
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError) as e:
|
||||
logger.info(
|
||||
"Could not query KWin VirtualKeyboard (%s); treating IBus as unbridged.",
|
||||
e,
|
||||
)
|
||||
return False
|
||||
for prop in ("available", "enabled"):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gdbus",
|
||||
"call",
|
||||
"--session",
|
||||
"--dest",
|
||||
"org.kde.KWin",
|
||||
"--object-path",
|
||||
"/VirtualKeyboard",
|
||||
"--method",
|
||||
"org.freedesktop.DBus.Properties.Get",
|
||||
"org.kde.kwin.VirtualKeyboard",
|
||||
prop,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2,
|
||||
env=host_env(),
|
||||
)
|
||||
except (subprocess.SubprocessError, FileNotFoundError) as e:
|
||||
logger.info(
|
||||
"Could not query KWin VirtualKeyboard (%s); treating IBus as unbridged.",
|
||||
e,
|
||||
)
|
||||
return False
|
||||
|
||||
out = (result.stdout or "").strip().lower() if result.returncode == 0 else ""
|
||||
# gdbus prints variant wrappers like: (<<true>>,) or (<<false>>,)
|
||||
if "<<true>>" in out:
|
||||
return True
|
||||
if "<<false>>" in out:
|
||||
logger.info(
|
||||
"KWin Virtual Keyboard is disabled; IBus commits will not reach "
|
||||
"native apps. Falling back to ydotool/wtype. Enable: System "
|
||||
"Settings → Keyboard → Virtual Keyboard → IBus Wayland."
|
||||
out = (result.stdout or "").strip().lower() if result.returncode == 0 else ""
|
||||
# gdbus prints the variant as (<true>,) on KWin 6 and doubly
|
||||
# wrapped as (<<true>>,) on Plasma 5; the bare "<true>" substring
|
||||
# matches both forms.
|
||||
if "<true>" in out:
|
||||
return True
|
||||
if "<false>" in out:
|
||||
logger.info(
|
||||
"KWin Virtual Keyboard is disabled; IBus commits will not reach "
|
||||
"native apps. Falling back to ydotool/wtype. Enable: System "
|
||||
"Settings → Keyboard → Virtual Keyboard → IBus Wayland."
|
||||
)
|
||||
return False
|
||||
# The property does not exist on this KWin (Plasma 6 dropped
|
||||
# 'enabled'; Plasma 5 lacks 'available') or its answer was not a
|
||||
# readable boolean -- try the other name before giving up.
|
||||
logger.debug(
|
||||
"KWin VirtualKeyboard property '%s' inconclusive (rc=%s out=%r).",
|
||||
prop,
|
||||
result.returncode,
|
||||
result.stdout,
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(
|
||||
"KWin VirtualKeyboard not confirmed (rc=%s out=%r); treating IBus as unbridged.",
|
||||
result.returncode,
|
||||
result.stdout,
|
||||
)
|
||||
logger.info("KWin VirtualKeyboard not confirmed; treating IBus as unbridged.")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@@ -879,14 +889,16 @@ class TextInjector:
|
||||
)
|
||||
# Bridging Wayland DEs (GNOME): inject_text() switches to the
|
||||
# real vocalinux engine for each commit, so a bare xkb:* baseline is
|
||||
# fine (#501, #504). KDE is not in that set unless IBus is already
|
||||
# the session IM: a leftover daemon plus scoped activate reports
|
||||
# success while Kate/Qt get nothing (#752). Unbridged compositors
|
||||
# still bail below.
|
||||
# fine (#501, #504). KDE joins that set only when KWin's Virtual
|
||||
# Keyboard is confirmed on: a confirmed VK bridge is what makes the
|
||||
# commits reach apps, and it is also what keeps the #752
|
||||
# leftover-daemon trap (scoped activate reports success while
|
||||
# Kate/Qt get nothing) excluded when the VK check fails (#911).
|
||||
# Unbridged compositors still bail below.
|
||||
wayland_scoped_ibus = (
|
||||
self.environment == DesktopEnvironment.WAYLAND
|
||||
and not explicit_non_ibus_im
|
||||
and not _is_kde_plasma_session()
|
||||
and (not _is_kde_plasma_session() or self._kde_virtual_keyboard_enabled())
|
||||
)
|
||||
|
||||
# Check if IBus is the active input method (not just installed)
|
||||
@@ -970,7 +982,11 @@ class TextInjector:
|
||||
# The RemoteDesktop portal sits ahead of both in autodetection: it
|
||||
# is the only injection path Wayland sanctions, works inside the
|
||||
# Flatpak sandbox without /dev/uinput, and reaches native clients
|
||||
# on compositors with no input-method-v2 bridge.
|
||||
# on compositors with no input-method-v2 bridge. KDE is the
|
||||
# exception: KWin's portal scrambles letter case (#911), so on
|
||||
# Plasma a present ydotool goes first -- the ordering from before
|
||||
# the portal backend existed.
|
||||
kde_ydotool_first = ydotool_available and _is_kde_plasma_session()
|
||||
if forced == "portal" and portal_available:
|
||||
self._select_portal_backend(
|
||||
"%s=portal: using RemoteDesktop portal for Wayland injection",
|
||||
@@ -983,13 +999,21 @@ class TextInjector:
|
||||
self._ensure_ydotoold()
|
||||
self.wayland_tool = "ydotool"
|
||||
logger.info("%s=ydotool: using ydotool for Wayland injection", pin_source)
|
||||
elif portal_available:
|
||||
elif portal_available and not kde_ydotool_first:
|
||||
self._select_portal_backend(
|
||||
"Using the RemoteDesktop portal for Wayland text injection"
|
||||
)
|
||||
elif ydotool_available and self._ensure_ydotoold():
|
||||
self.wayland_tool = "ydotool"
|
||||
logger.info("Using ydotool for Wayland text injection")
|
||||
elif portal_available:
|
||||
# Reached only on KDE after ydotool proved unusable; on other
|
||||
# desktops the earlier portal branch already fired. A daemonless
|
||||
# ydotool must not outrank a working portal: it may fail at
|
||||
# injection time (no /dev/uinput), while the portal delivers.
|
||||
self._select_portal_backend(
|
||||
"Using the RemoteDesktop portal for Wayland text injection"
|
||||
)
|
||||
elif ydotool_available and not wtype_available:
|
||||
self.wayland_tool = "ydotool"
|
||||
logger.warning(
|
||||
|
||||
+258
-16
@@ -2558,9 +2558,9 @@ class TestCompositorIBusBridging(unittest.TestCase):
|
||||
self.assertTrue(injector._wayland_compositor_bridges_ibus())
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.subprocess.run")
|
||||
def test_kde_virtual_keyboard_enabled_parses_gdbus_true(self, mock_run):
|
||||
"""gdbus (<<true>>,) means VirtualKeyboard is enabled."""
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<<true>>,)\n", stderr="")
|
||||
def test_kde_virtual_keyboard_enabled_parses_gdbus_true(self, mock_run: MagicMock) -> None:
|
||||
"""gdbus (<true>,) on 'available' means VirtualKeyboard is enabled (#911)."""
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<true>,)\n", stderr="")
|
||||
injector = self._bare_injector()
|
||||
self.assertTrue(injector._kde_virtual_keyboard_enabled())
|
||||
mock_run.assert_called_once()
|
||||
@@ -2568,19 +2568,68 @@ class TestCompositorIBusBridging(unittest.TestCase):
|
||||
self.assertEqual(args[0], "gdbus")
|
||||
self.assertIn("org.kde.KWin", args)
|
||||
self.assertIn("/VirtualKeyboard", args)
|
||||
# KWin 6 exposes the setting as 'available'; it must be queried first.
|
||||
self.assertEqual(args[-1], "available")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.subprocess.run")
|
||||
def test_kde_virtual_keyboard_enabled_parses_gdbus_false(self, mock_run):
|
||||
"""gdbus (<<false>>,) means VirtualKeyboard is disabled."""
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<<false>>,)\n", stderr="")
|
||||
def test_kde_virtual_keyboard_enabled_parses_gdbus_false(self, mock_run: MagicMock) -> None:
|
||||
"""gdbus (<false>,) means VirtualKeyboard is disabled."""
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<false>,)\n", stderr="")
|
||||
injector = self._bare_injector()
|
||||
self.assertFalse(injector._kde_virtual_keyboard_enabled())
|
||||
mock_run.assert_called_once()
|
||||
self.assertEqual(mock_run.call_args[0][0][-1], "available")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.subprocess.run")
|
||||
def test_kde_virtual_keyboard_accepts_legacy_wrapped_true(self, mock_run: MagicMock) -> None:
|
||||
"""Plasma 5's doubly wrapped (<<true>>,) answer form is also accepted."""
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<<true>>,)\n", stderr="")
|
||||
injector = self._bare_injector()
|
||||
self.assertTrue(injector._kde_virtual_keyboard_enabled())
|
||||
mock_run.assert_called_once()
|
||||
self.assertEqual(mock_run.call_args[0][0][-1], "available")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.subprocess.run")
|
||||
def test_kde_virtual_keyboard_falls_back_to_enabled_property(self, mock_run: MagicMock) -> None:
|
||||
"""Plasma 5 has no 'available': 'enabled' is queried next (#911)."""
|
||||
mock_run.side_effect = [
|
||||
MagicMock(returncode=1, stdout="", stderr="Error: UnknownProperty"),
|
||||
MagicMock(returncode=0, stdout="(<<true>>,)\n", stderr=""),
|
||||
]
|
||||
injector = self._bare_injector()
|
||||
self.assertTrue(injector._kde_virtual_keyboard_enabled())
|
||||
self.assertEqual(mock_run.call_count, 2)
|
||||
self.assertEqual(mock_run.call_args_list[0][0][0][-1], "available")
|
||||
self.assertEqual(mock_run.call_args_list[1][0][0][-1], "enabled")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.subprocess.run")
|
||||
def test_kde_virtual_keyboard_enabled_property_false(self, mock_run: MagicMock) -> None:
|
||||
"""The 'enabled' fallback answering (<<false>>,) still means disabled."""
|
||||
mock_run.side_effect = [
|
||||
MagicMock(returncode=1, stdout="", stderr="Error: UnknownProperty"),
|
||||
MagicMock(returncode=0, stdout="(<<false>>,)\n", stderr=""),
|
||||
]
|
||||
injector = self._bare_injector()
|
||||
self.assertFalse(injector._kde_virtual_keyboard_enabled())
|
||||
self.assertEqual(mock_run.call_count, 2)
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.subprocess.run")
|
||||
def test_kde_virtual_keyboard_false_when_neither_property_exists(
|
||||
self, mock_run: MagicMock
|
||||
) -> None:
|
||||
"""No readable answer from either property → treat as unbridged."""
|
||||
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="Error: UnknownProperty")
|
||||
injector = self._bare_injector()
|
||||
self.assertFalse(injector._kde_virtual_keyboard_enabled())
|
||||
self.assertEqual(mock_run.call_count, 2)
|
||||
|
||||
@patch(
|
||||
"vocalinux.text_injection.text_injector.subprocess.run",
|
||||
side_effect=FileNotFoundError("gdbus"),
|
||||
)
|
||||
def test_kde_virtual_keyboard_enabled_false_when_gdbus_missing(self, _mock_run):
|
||||
def test_kde_virtual_keyboard_enabled_false_when_gdbus_missing(
|
||||
self, _mock_run: MagicMock
|
||||
) -> None:
|
||||
"""Missing gdbus → treat as unbridged (conservative)."""
|
||||
injector = self._bare_injector()
|
||||
self.assertFalse(injector._kde_virtual_keyboard_enabled())
|
||||
@@ -2611,7 +2660,7 @@ class TestCompositorIBusBridging(unittest.TestCase):
|
||||
"ydotool": "/usr/bin/ydotool",
|
||||
"ydotoold": "/usr/bin/ydotoold",
|
||||
}.get(cmd)
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<<false>>,)\n", stderr="")
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<false>,)\n", stderr="")
|
||||
|
||||
with (
|
||||
patch.dict(
|
||||
@@ -2659,7 +2708,7 @@ class TestCompositorIBusBridging(unittest.TestCase):
|
||||
"ydotool": "/usr/bin/ydotool",
|
||||
"ydotoold": "/usr/bin/ydotoold",
|
||||
}.get(cmd)
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<<true>>,)\n", stderr="")
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="(<true>,)\n", stderr="")
|
||||
mock_ibus_class.return_value = MagicMock()
|
||||
|
||||
with patch.dict(
|
||||
@@ -2865,6 +2914,154 @@ class TestCompositorIBusBridging(unittest.TestCase):
|
||||
injector = TextInjector()
|
||||
self.assertEqual(injector.wayland_tool, "ydotool")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=False)
|
||||
@patch("vocalinux.text_injection.text_injector.shutil.which")
|
||||
def test_kde_auto_prefers_ydotool_over_portal(
|
||||
self, mock_which: MagicMock, _mock_ibus: MagicMock
|
||||
) -> None:
|
||||
"""KWin's portal scrambles letter case: on KDE, ydotool goes first (#911)."""
|
||||
mock_which.side_effect = lambda cmd: {
|
||||
"ydotool": "/usr/bin/ydotool",
|
||||
"ydotoold": "/usr/bin/ydotoold",
|
||||
}.get(cmd)
|
||||
with (
|
||||
patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"XDG_SESSION_TYPE": "wayland",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
"XDG_SESSION_DESKTOP": "KDE",
|
||||
"DESKTOP_SESSION": "plasma",
|
||||
"KDE_FULL_SESSION": "true",
|
||||
},
|
||||
clear=True,
|
||||
),
|
||||
patch.object(TextInjector, "_portal_probe", return_value=True),
|
||||
patch.object(TextInjector, "_is_ydotoold_running", return_value=True),
|
||||
):
|
||||
injector = TextInjector()
|
||||
self.assertEqual(injector.wayland_tool, "ydotool")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=False)
|
||||
@patch("vocalinux.text_injection.text_injector.shutil.which")
|
||||
def test_non_kde_auto_still_prefers_portal_over_ydotool(
|
||||
self, mock_which: MagicMock, _mock_ibus: MagicMock
|
||||
) -> None:
|
||||
"""The ydotool-first reorder is KDE-only; other desktops keep the portal."""
|
||||
mock_which.side_effect = lambda cmd: {
|
||||
"ydotool": "/usr/bin/ydotool",
|
||||
"ydotoold": "/usr/bin/ydotoold",
|
||||
}.get(cmd)
|
||||
with (
|
||||
patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"XDG_SESSION_TYPE": "wayland",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_CURRENT_DESKTOP": "GNOME",
|
||||
"XDG_SESSION_DESKTOP": "GNOME",
|
||||
"DESKTOP_SESSION": "gnome",
|
||||
"KDE_FULL_SESSION": "",
|
||||
},
|
||||
clear=True,
|
||||
),
|
||||
patch.object(TextInjector, "_portal_probe", return_value=True),
|
||||
patch.object(TextInjector, "_is_ydotoold_running", return_value=True),
|
||||
):
|
||||
injector = TextInjector()
|
||||
self.assertEqual(injector.wayland_tool, "portal")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=False)
|
||||
@patch("vocalinux.text_injection.text_injector.shutil.which")
|
||||
def test_kde_auto_uses_portal_when_ydotool_missing(
|
||||
self, mock_which: MagicMock, _mock_ibus: MagicMock
|
||||
) -> None:
|
||||
"""The KDE reorder needs ydotool installed; without it the portal still wins."""
|
||||
mock_which.side_effect = lambda cmd: None
|
||||
with (
|
||||
patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"XDG_SESSION_TYPE": "wayland",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
"XDG_SESSION_DESKTOP": "KDE",
|
||||
"DESKTOP_SESSION": "plasma",
|
||||
"KDE_FULL_SESSION": "true",
|
||||
},
|
||||
clear=True,
|
||||
),
|
||||
patch.object(TextInjector, "_portal_probe", return_value=True),
|
||||
):
|
||||
injector = TextInjector()
|
||||
self.assertEqual(injector.wayland_tool, "portal")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=False)
|
||||
@patch("vocalinux.text_injection.text_injector.shutil.which")
|
||||
def test_kde_auto_uses_portal_when_ydotoold_unusable(
|
||||
self, mock_which: MagicMock, _mock_ibus: MagicMock
|
||||
) -> None:
|
||||
"""On KDE the portal is still the fallback when ydotool cannot run (#911)."""
|
||||
mock_which.side_effect = lambda cmd: {
|
||||
"ydotool": "/usr/bin/ydotool",
|
||||
"ydotoold": "/usr/bin/ydotoold",
|
||||
"wtype": "/usr/bin/wtype",
|
||||
}.get(cmd)
|
||||
with (
|
||||
patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"XDG_SESSION_TYPE": "wayland",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
"XDG_SESSION_DESKTOP": "KDE",
|
||||
"DESKTOP_SESSION": "plasma",
|
||||
"KDE_FULL_SESSION": "true",
|
||||
},
|
||||
clear=True,
|
||||
),
|
||||
patch.object(TextInjector, "_portal_probe", return_value=True),
|
||||
patch.object(TextInjector, "_is_ydotoold_running", return_value=False),
|
||||
patch.object(TextInjector, "_uinput_usable", return_value=False),
|
||||
):
|
||||
injector = TextInjector()
|
||||
self.assertEqual(injector.wayland_tool, "portal")
|
||||
|
||||
@patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=False)
|
||||
@patch("vocalinux.text_injection.text_injector.shutil.which")
|
||||
def test_kde_auto_prefers_portal_over_daemonless_ydotool(
|
||||
self, mock_which: MagicMock, _mock_ibus: MagicMock
|
||||
) -> None:
|
||||
"""KDE + unusable ydotool + no wtype: portal beats daemonless ydotool (#911).
|
||||
|
||||
The daemonless-ydotool branch would pick a backend that fails at
|
||||
injection time (no /dev/uinput) while a working portal sits unused.
|
||||
"""
|
||||
mock_which.side_effect = lambda cmd: {
|
||||
"ydotool": "/usr/bin/ydotool",
|
||||
"ydotoold": "/usr/bin/ydotoold",
|
||||
}.get(cmd)
|
||||
with (
|
||||
patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"XDG_SESSION_TYPE": "wayland",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
"XDG_SESSION_DESKTOP": "KDE",
|
||||
"DESKTOP_SESSION": "plasma",
|
||||
"KDE_FULL_SESSION": "true",
|
||||
},
|
||||
clear=True,
|
||||
),
|
||||
patch.object(TextInjector, "_portal_probe", return_value=True),
|
||||
patch.object(TextInjector, "_is_ydotoold_running", return_value=False),
|
||||
patch.object(TextInjector, "_uinput_usable", return_value=False),
|
||||
):
|
||||
injector = TextInjector()
|
||||
self.assertEqual(injector.wayland_tool, "portal")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _fake_config(config: Any) -> Iterator[None]:
|
||||
@@ -3107,7 +3304,11 @@ class TestForcedBackend(unittest.TestCase):
|
||||
|
||||
|
||||
class TestKdeSkipsInactiveIbus(unittest.TestCase):
|
||||
"""KDE without IBus as the session IM must not take the scoped IBus path (#752)."""
|
||||
"""KDE's scoped-IBus admission now hinges on KWin VirtualKeyboard (#752, #911).
|
||||
|
||||
A leftover ibus-daemon on KDE reports success while apps get nothing, so an
|
||||
inactive IBus is only trusted when the VK bridge is confirmed on.
|
||||
"""
|
||||
|
||||
@patch(
|
||||
"vocalinux.text_injection.text_injector.is_ibus_daemon_running",
|
||||
@@ -3122,10 +3323,10 @@ class TestKdeSkipsInactiveIbus(unittest.TestCase):
|
||||
@patch("vocalinux.text_injection.text_injector.shutil.which")
|
||||
def test_kde_wayland_does_not_construct_ibus_injector(
|
||||
self,
|
||||
mock_which,
|
||||
mock_ibus_class,
|
||||
*_args,
|
||||
):
|
||||
mock_which: MagicMock,
|
||||
mock_ibus_class: MagicMock,
|
||||
*_args: MagicMock,
|
||||
) -> None:
|
||||
mock_which.side_effect = lambda cmd: ("/usr/bin/wtype" if cmd == "wtype" else None)
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
@@ -3137,6 +3338,47 @@ class TestKdeSkipsInactiveIbus(unittest.TestCase):
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
injector = TextInjector()
|
||||
with patch.object(TextInjector, "_kde_virtual_keyboard_enabled", return_value=False):
|
||||
TextInjector()
|
||||
mock_ibus_class.assert_not_called()
|
||||
self.assertIsNone(injector._ibus_injector)
|
||||
|
||||
@patch(
|
||||
"vocalinux.text_injection.text_injector.is_ibus_daemon_running",
|
||||
return_value=True,
|
||||
)
|
||||
@patch(
|
||||
"vocalinux.text_injection.text_injector.is_ibus_active_input_method",
|
||||
return_value=False,
|
||||
)
|
||||
@patch("vocalinux.text_injection.text_injector.is_ibus_available", return_value=True)
|
||||
@patch("vocalinux.text_injection.text_injector.IBusTextInjector")
|
||||
@patch("vocalinux.text_injection.text_injector.shutil.which")
|
||||
def test_kde_wayland_with_virtual_keyboard_constructs_ibus_injector(
|
||||
self,
|
||||
mock_which: MagicMock,
|
||||
mock_ibus_class: MagicMock,
|
||||
*_args: MagicMock,
|
||||
) -> None:
|
||||
"""KDE + confirmed VK bridge + inactive IBus → scoped IBus path (#911).
|
||||
|
||||
With KWin VirtualKeyboard set to "IBus Wayland" the bare-xkb baseline is
|
||||
safe: the bridge is exactly what the #752 exclusion was uncertain about.
|
||||
"""
|
||||
mock_which.side_effect = lambda cmd: ("/usr/bin/wtype" if cmd == "wtype" else None)
|
||||
mock_ibus_class.return_value = MagicMock()
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"XDG_SESSION_TYPE": "wayland",
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
"KDE_FULL_SESSION": "true",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
with patch.object(TextInjector, "_kde_virtual_keyboard_enabled", return_value=True):
|
||||
injector = TextInjector()
|
||||
if injector._ibus_init_thread is not None:
|
||||
injector._ibus_init_thread.join(timeout=5)
|
||||
mock_ibus_class.assert_called_once()
|
||||
self.assertEqual(injector.environment, DesktopEnvironment.WAYLAND_IBUS)
|
||||
|
||||
Reference in New Issue
Block a user