fix(input): clarify KDE Wayland IBus setup (#466)

This commit is contained in:
Jatin K Malik
2026-06-10 16:08:11 -07:00
committed by GitHub
parent a73eec4a0a
commit 9c5065f452
5 changed files with 224 additions and 14 deletions
+8 -6
View File
@@ -106,8 +106,9 @@ The installer now installs these automatically, but if you hit a CMake error lik
`Could not find OpenSSL` during a manual reinstall, add the above first.
**ydotool** — `ydotool` is not packaged in Debian's standard repos. The installer falls back
gracefully to `wtype` for most Wayland compositors. If you specifically need `ydotool`
(e.g. for KDE Plasma Wayland), compile it from source:
gracefully to IBus or `wtype` for most Wayland compositors. KDE Plasma Wayland users should
first select **IBus Wayland** in **System Settings -> Keyboard -> Virtual Keyboard**. If you
specifically need `ydotool` as a fallback, compile it from source:
```bash
sudo apt install -y git cmake libevdev-dev
@@ -358,8 +359,9 @@ Vocalinux requires text injection tools to work with X11 or Wayland:
- **xdotool**: Required for X11 sessions
### For Wayland
- **wtype**: Recommended for Wayland native sessions
- **ydotool**: Universal alternative that works with both X11 and Wayland
- **IBus**: Recommended for KDE Plasma Wayland and generally the most reliable direct text input path
- **wtype**: Recommended for wlroots-style Wayland compositors such as sway
- **ydotool**: Universal alternative that works with both X11 and Wayland, but requires `ydotoold`
- **xdotool**: May work via XWayland fallback
### Installation by Distribution
@@ -399,7 +401,7 @@ For unsupported or experimental distributions:
| Desktop Environment | Status | Notes |
|---------------------|--------|-------|
| GNOME | ✅ Full Support | Primary target, uses AppIndicator |
| KDE Plasma | ✅ Full Support | Works with AppIndicator |
| KDE Plasma | ✅ Full Support | On Wayland, select IBus Wayland in System Settings -> Keyboard -> Virtual Keyboard |
| Xfce | ✅ Full Support | Works with AppIndicator |
| Cinnamon | ✅ Full Support | Works with AppIndicator |
| MATE | ✅ Full Support | Works with AppIndicator |
@@ -428,7 +430,7 @@ Practical caveat:
| Display Server | Status | Notes |
|----------------|--------|-------|
| X11 | ✅ Full Support | Uses xdotool for text injection |
| Wayland | ✅ Full Support | Uses wtype (native) or xdotool (XWayland) |
| Wayland | ✅ Full Support | Uses IBus, wtype, ydotool, or xdotool fallback depending on compositor |
| XWayland | ✅ Full Support | Automatically detected, falls back to xdotool if needed |
## Architecture Support
+21
View File
@@ -559,12 +559,33 @@ sudo apt install xdotool
# Test: xdotool type "hello"
```
For KDE Plasma Wayland:
1. Install IBus if it is missing:
```bash
# Ubuntu/Debian
sudo apt install ibus
# Fedora
sudo dnf install ibus
# Arch
sudo pacman -S ibus
```
2. Open **System Settings -> Keyboard -> Virtual Keyboard**.
3. Select **IBus Wayland**.
4. Restart Vocalinux, or log out and back in if text still does not appear.
For Wayland:
```bash
sudo apt install wtype
# Test: wtype "hello"
```
On KDE Plasma Wayland, `wtype` may fail because the compositor does not expose
the required virtual keyboard protocol to regular clients. Use **IBus Wayland**
from KDE System Settings for the most reliable direct text injection.
### Model Download Fails
**Symptom:** Can't download speech models
+18
View File
@@ -29,6 +29,21 @@ command_exists() {
command -v "$1" >/dev/null 2>&1
}
is_kde_plasma_session() {
local desktop="${XDG_CURRENT_DESKTOP:-} ${DESKTOP_SESSION:-} ${GDMSESSION:-}"
local kde_session="${KDE_FULL_SESSION:-}"
local desktop_lower="${desktop,,}"
local kde_session_lower="${kde_session,,}"
[[ "$desktop_lower" == *kde* || "$desktop_lower" == *plasma* || "$kde_session_lower" == "true" ]]
}
print_kde_wayland_ibus_hint() {
print_warning "KDE Plasma Wayland detected."
print_info "For direct dictation into apps, open System Settings -> Keyboard -> Virtual Keyboard and select 'IBus Wayland'."
print_info "After changing it, restart Vocalinux or log out and back in."
}
get_vocalinux_pids() {
pgrep -f "vocalinux" 2>/dev/null | while read -r pid; do
[ -z "$pid" ] && continue
@@ -1900,6 +1915,9 @@ install_text_input_tools() {
fi
print_info "Detected session type: $SESSION_TYPE"
if [[ "$SESSION_TYPE" == "wayland" ]] && is_kde_plasma_session; then
print_kde_wayland_ibus_hint
fi
# Install appropriate tools based on session type and distribution
case "$SESSION_TYPE" in
+54 -8
View File
@@ -24,6 +24,28 @@ from .ibus_engine import (
logger = logging.getLogger(__name__)
def _is_kde_plasma_session() -> bool:
"""Return True when the current desktop session appears to be KDE Plasma."""
if os.environ.get("KDE_FULL_SESSION", "").lower() == "true":
return True
desktop_values = [
os.environ.get("XDG_CURRENT_DESKTOP", ""),
os.environ.get("DESKTOP_SESSION", ""),
os.environ.get("GDMSESSION", ""),
]
desktop = " ".join(value for value in desktop_values if value).lower()
return "kde" in desktop or "plasma" in desktop
def _kde_wayland_ibus_hint() -> str:
"""Return the KDE Plasma Wayland IBus setup hint."""
return (
"Open System Settings -> Keyboard -> Virtual Keyboard, select "
"'IBus Wayland', then restart Vocalinux or log out and back in."
)
class DesktopEnvironment(Enum):
"""Enum representing the desktop environment."""
@@ -88,6 +110,11 @@ class TextInjector:
"Wayland compositor does not support virtual "
f"keyboard protocol: {error_output}"
)
if _is_kde_plasma_session():
logger.warning(
"KDE Plasma Wayland detected. wtype is not a reliable "
f"text injection path on this compositor. {_kde_wayland_ibus_hint()}"
)
if shutil.which("xdotool"):
logger.info("Automatically switching to XWayland fallback with xdotool")
self.environment = DesktopEnvironment.WAYLAND_XDOTOOL
@@ -153,10 +180,17 @@ class TextInjector:
# This is important because IBus may be installed but not being used,
# e.g., when the user has configured ydotool or Fcitx instead
if not is_ibus_active_input_method():
logger.info(
"IBus is installed but not the active input method. "
"Falling back to alternative text injection method."
)
if self.environment == DesktopEnvironment.WAYLAND and _is_kde_plasma_session():
logger.warning(
"IBus is installed but is not active for KDE Plasma Wayland. "
f"{_kde_wayland_ibus_hint()} Falling back to alternative "
"text injection method."
)
else:
logger.info(
"IBus is installed but not the active input method. "
"Falling back to alternative text injection method."
)
# Check if ibus-daemon is running before attempting setup
elif not is_ibus_daemon_running():
logger.info(
@@ -230,7 +264,8 @@ class TextInjector:
"- xdotool: sudo apt install xdotool (X11/XWayland only)\n"
"\n"
"For KDE Plasma Wayland users: wtype is not supported. "
"Install ydotool or wl-copy for clipboard fallback:\n"
f"{_kde_wayland_ibus_hint()}\n"
"Or install ydotool/wl-copy for fallback:\n"
" sudo apt install ydotool\n"
" sudo systemctl enable --now ydotoold\n"
"Or for clipboard fallback: sudo apt install wl-copy"
@@ -619,12 +654,23 @@ class TextInjector:
self._inject_with_wayland_tool(text)
except subprocess.CalledProcessError as e:
stderr_msg = e.stderr.strip() if e.stderr else "No stderr output"
unsupported_wayland = (
"compositor does not support" in str(e).lower()
or "compositor does not support" in stderr_msg.lower()
)
logger.warning(
f"Wayland tool failed: {e}. stderr: {stderr_msg}. Falling back to xdotool"
)
if "compositor does not support" in str(
e
).lower() + " " + stderr_msg.lower() and shutil.which("xdotool"):
if (
unsupported_wayland
and current_env == DesktopEnvironment.WAYLAND
and _is_kde_plasma_session()
):
logger.warning(
"KDE Plasma Wayland rejected virtual keyboard injection. "
f"{_kde_wayland_ibus_hint()}"
)
if unsupported_wayland and shutil.which("xdotool"):
logger.info(
"Switching to XWayland fallback - will re-check for better tools"
)
+123
View File
@@ -40,6 +40,32 @@ class TestDesktopEnvironmentEnum(unittest.TestCase):
self.assertEqual(DesktopEnvironment.UNKNOWN.value, "unknown")
class TestKdePlasmaDetection(unittest.TestCase):
def test_detects_xdg_current_desktop_kde(self):
from vocalinux.text_injection.text_injector import _is_kde_plasma_session
with patch.dict(os.environ, {"XDG_CURRENT_DESKTOP": "KDE"}, clear=True):
self.assertTrue(_is_kde_plasma_session())
def test_detects_kde_full_session(self):
from vocalinux.text_injection.text_injector import _is_kde_plasma_session
with patch.dict(os.environ, {"KDE_FULL_SESSION": "true"}, clear=True):
self.assertTrue(_is_kde_plasma_session())
def test_detects_desktop_session_plasma(self):
from vocalinux.text_injection.text_injector import _is_kde_plasma_session
with patch.dict(os.environ, {"DESKTOP_SESSION": "plasma"}, clear=True):
self.assertTrue(_is_kde_plasma_session())
def test_ignores_non_kde_session(self):
from vocalinux.text_injection.text_injector import _is_kde_plasma_session
with patch.dict(os.environ, {"XDG_CURRENT_DESKTOP": "GNOME"}, clear=True):
self.assertFalse(_is_kde_plasma_session())
def _make_injector(env) -> Any:
from vocalinux.text_injection.text_injector import TextInjector
@@ -124,6 +150,75 @@ class TestCheckDependencies(unittest.TestCase):
with self.assertRaises(RuntimeError):
obj._check_dependencies()
def test_kde_wayland_inactive_ibus_logs_virtual_keyboard_hint(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment
obj = _make_injector(DesktopEnvironment.WAYLAND)
with patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "XDG_CURRENT_DESKTOP": "KDE"},
clear=True,
):
with patch(
"vocalinux.text_injection.text_injector.is_ibus_available",
return_value=True,
):
with patch(
"vocalinux.text_injection.text_injector.is_ibus_active_input_method",
return_value=False,
):
with patch(
"shutil.which",
side_effect=lambda cmd: "/usr/bin/ydotool" if cmd == "ydotool" else None,
):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stderr="")
with self.assertLogs(
"vocalinux.text_injection.text_injector",
level="WARNING",
) as logs:
obj._check_dependencies()
log_output = "\n".join(logs.output)
self.assertIn("KDE Plasma Wayland", log_output)
self.assertIn("Virtual Keyboard", log_output)
def test_kde_wayland_wtype_probe_logs_ibus_hint(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment, TextInjector
def which(cmd):
if cmd in {"wtype", "xdotool"}:
return f"/usr/bin/{cmd}"
return None
with patch.dict(
os.environ,
{"XDG_SESSION_TYPE": "wayland", "XDG_CURRENT_DESKTOP": "KDE"},
clear=True,
):
with patch(
"vocalinux.text_injection.text_injector.is_ibus_available",
return_value=False,
):
with patch("shutil.which", side_effect=which):
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(
returncode=1,
stderr="compositor does not support virtual keyboard",
)
with patch.object(TextInjector, "_test_xdotool_fallback"):
with self.assertLogs(
"vocalinux.text_injection.text_injector",
level="WARNING",
) as logs:
injector = TextInjector()
log_output = "\n".join(logs.output)
self.assertEqual(injector.environment, DesktopEnvironment.WAYLAND_XDOTOOL)
self.assertIn("KDE Plasma Wayland detected", log_output)
self.assertIn("IBus Wayland", log_output)
class TestInjectText(unittest.TestCase):
def test_inject_x11(self):
@@ -147,6 +242,34 @@ class TestInjectText(unittest.TestCase):
self.assertTrue(mock_run.called)
self.assertTrue(result)
def test_kde_wayland_wtype_failure_logs_ibus_hint_and_uses_xdotool(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment
obj = _make_injector(DesktopEnvironment.WAYLAND)
obj.wayland_tool = "wtype"
error = subprocess.CalledProcessError(
1,
["wtype", "hello"],
stderr="compositor does not support virtual keyboard",
)
with patch.dict(os.environ, {"XDG_CURRENT_DESKTOP": "KDE"}, clear=True):
with patch.object(obj, "_inject_with_wayland_tool", side_effect=error):
with patch.object(obj, "_inject_with_xdotool") as mock_xdotool:
with patch("shutil.which", return_value="/usr/bin/xdotool"):
with self.assertLogs(
"vocalinux.text_injection.text_injector",
level="WARNING",
) as logs:
result = obj.inject_text("hello")
self.assertTrue(result)
self.assertEqual(obj.environment, DesktopEnvironment.WAYLAND_XDOTOOL)
mock_xdotool.assert_called_once_with("hello")
log_output = "\n".join(logs.output)
self.assertIn("KDE Plasma Wayland rejected virtual keyboard injection", log_output)
self.assertIn("IBus Wayland", log_output)
def test_inject_ibus(self):
from vocalinux.text_injection.text_injector import DesktopEnvironment