fix(input): clarify KDE Wayland IBus setup (#466)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user