* fix(injection): use Ctrl+Shift+V when pasting into terminals Clipboard injection always sent Ctrl+V, which most terminal emulators ignore. Detect standalone terminal windows and send Ctrl+Shift+V, with a Settings override for nested IDE panels that window-class detection cannot see. * fix(injection): fail closed when terminal window probe is mocked Clipboard tests patch text_injector.subprocess.run, which replaces stdlib subprocess.run. Window detection now ignores non-string probe output so injection still uses Ctrl+V instead of raising TypeError. * docs: fix overclaimed terminal auto-detect in USER_GUIDE Auto-detect only runs on X11 and on Hyprland, Sway, and niri. GNOME and KDE Wayland stay on Ctrl+V unless you set Clipboard Paste Shortcut to Ctrl+Shift+V.
159 lines
7.1 KiB
Python
159 lines
7.1 KiB
Python
"""Tests for focused-window identity and terminal detection."""
|
|
|
|
import json
|
|
import unittest
|
|
from unittest.mock import MagicMock, mock_open, patch
|
|
|
|
from vocalinux.text_injection.focused_window import (
|
|
FocusedWindow,
|
|
get_focused_window,
|
|
is_focused_window_terminal,
|
|
looks_like_terminal,
|
|
)
|
|
|
|
|
|
class TestLooksLikeTerminal(unittest.TestCase):
|
|
"""Unit tests for standalone terminal identity matching."""
|
|
|
|
def test_known_terminal_process(self):
|
|
self.assertTrue(looks_like_terminal(FocusedWindow(process_name="kitty")))
|
|
|
|
def test_gnome_terminal_app_id(self):
|
|
self.assertTrue(looks_like_terminal(FocusedWindow(app_id="org.gnome.Terminal")))
|
|
|
|
def test_ghostty_class(self):
|
|
self.assertTrue(looks_like_terminal(FocusedWindow(wm_class="com.mitchellh.ghostty")))
|
|
|
|
def test_xfce_terminal_class(self):
|
|
self.assertTrue(looks_like_terminal(FocusedWindow(wm_class="Xfce4-terminal")))
|
|
|
|
def test_st_is_exact_token_only(self):
|
|
self.assertTrue(looks_like_terminal(FocusedWindow(process_name="st")))
|
|
self.assertFalse(looks_like_terminal(FocusedWindow(process_name="steam")))
|
|
|
|
def test_editor_is_not_a_terminal(self):
|
|
self.assertFalse(
|
|
looks_like_terminal(
|
|
FocusedWindow(app_id="code", title="terminal.py — Visual Studio Code")
|
|
)
|
|
)
|
|
|
|
def test_cursor_nested_terminal_panel_is_not_auto_detected(self):
|
|
self.assertFalse(
|
|
looks_like_terminal(FocusedWindow(app_id="cursor", title="bash — Terminal"))
|
|
)
|
|
|
|
def test_browser_is_not_a_terminal(self):
|
|
self.assertFalse(looks_like_terminal(FocusedWindow(app_id="firefox", title="Terminal")))
|
|
|
|
def test_empty_window_is_not_a_terminal(self):
|
|
self.assertFalse(looks_like_terminal(FocusedWindow()))
|
|
|
|
def test_non_string_identity_is_ignored(self):
|
|
mock_value = MagicMock()
|
|
window = FocusedWindow(app_id=mock_value, wm_class=mock_value, process_name=mock_value)
|
|
self.assertFalse(looks_like_terminal(window))
|
|
|
|
|
|
class TestGetFocusedWindow(unittest.TestCase):
|
|
"""Probe helpers should fail closed and parse compositor JSON."""
|
|
|
|
def test_wayland_hyprland_active_window(self):
|
|
payload = {"class": "kitty", "title": "zsh", "pid": 4242}
|
|
with patch.dict("os.environ", {"WAYLAND_DISPLAY": "wayland-0"}, clear=False):
|
|
with patch("vocalinux.text_injection.focused_window.shutil.which") as mock_which:
|
|
mock_which.side_effect = lambda cmd: "/usr/bin/" + cmd if cmd == "hyprctl" else None
|
|
with patch("vocalinux.text_injection.focused_window.subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=json.dumps(payload))
|
|
with patch(
|
|
"vocalinux.text_injection.focused_window.open",
|
|
mock_open(read_data="kitty\n"),
|
|
):
|
|
window = get_focused_window()
|
|
self.assertIsNotNone(window)
|
|
assert window is not None
|
|
self.assertEqual(window.wm_class, "kitty")
|
|
self.assertEqual(window.process_name, "kitty")
|
|
self.assertTrue(looks_like_terminal(window))
|
|
|
|
def test_wayland_niri_focused_window(self):
|
|
payload = {"app_id": "Alacritty", "title": "nvim", "pid": 99}
|
|
with patch.dict("os.environ", {"WAYLAND_DISPLAY": "wayland-1"}, clear=False):
|
|
with patch("vocalinux.text_injection.focused_window.shutil.which") as mock_which:
|
|
mock_which.side_effect = lambda cmd: "/usr/bin/" + cmd if cmd == "niri" else None
|
|
with patch("vocalinux.text_injection.focused_window.subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=json.dumps(payload))
|
|
window = get_focused_window()
|
|
self.assertIsNotNone(window)
|
|
assert window is not None
|
|
self.assertEqual(window.app_id, "Alacritty")
|
|
self.assertTrue(looks_like_terminal(window))
|
|
|
|
def test_wayland_sway_walks_tree_for_focused_node(self):
|
|
tree = {
|
|
"nodes": [
|
|
{
|
|
"app_id": "firefox",
|
|
"focused": False,
|
|
"nodes": [
|
|
{
|
|
"app_id": "foot",
|
|
"name": "zsh",
|
|
"focused": True,
|
|
"floating_nodes": [],
|
|
"nodes": [],
|
|
}
|
|
],
|
|
"floating_nodes": [],
|
|
}
|
|
],
|
|
"floating_nodes": [],
|
|
}
|
|
with patch.dict("os.environ", {"WAYLAND_DISPLAY": "wayland-0"}, clear=False):
|
|
with patch("vocalinux.text_injection.focused_window.shutil.which") as mock_which:
|
|
mock_which.side_effect = lambda cmd: "/usr/bin/" + cmd if cmd == "swaymsg" else None
|
|
with patch("vocalinux.text_injection.focused_window.subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=json.dumps(tree))
|
|
window = get_focused_window()
|
|
self.assertIsNotNone(window)
|
|
assert window is not None
|
|
self.assertEqual(window.app_id, "foot")
|
|
self.assertTrue(looks_like_terminal(window))
|
|
|
|
def test_x11_uses_xdotool_class(self):
|
|
with patch.dict("os.environ", {"WAYLAND_DISPLAY": ""}, clear=False):
|
|
with patch("vocalinux.text_injection.focused_window.shutil.which") as mock_which:
|
|
mock_which.side_effect = lambda cmd: "/usr/bin/" + cmd if cmd == "xdotool" else None
|
|
with patch("vocalinux.text_injection.focused_window.subprocess.run") as mock_run:
|
|
|
|
def _run(cmd, **_kwargs):
|
|
mapping = {
|
|
("xdotool", "getactivewindow"): "12345",
|
|
("xdotool", "getwindowclassname", "12345"): "konsole",
|
|
("xdotool", "getwindowname", "12345"): "zsh",
|
|
("xdotool", "getwindowpid", "12345"): "77",
|
|
}
|
|
return MagicMock(returncode=0, stdout=mapping.get(tuple(cmd), ""))
|
|
|
|
mock_run.side_effect = _run
|
|
with patch(
|
|
"vocalinux.text_injection.focused_window.open",
|
|
mock_open(read_data="konsole\n"),
|
|
):
|
|
window = get_focused_window()
|
|
self.assertIsNotNone(window)
|
|
assert window is not None
|
|
self.assertEqual(window.wm_class, "konsole")
|
|
self.assertTrue(looks_like_terminal(window))
|
|
|
|
def test_probe_failure_returns_none(self):
|
|
with patch("vocalinux.text_injection.focused_window.shutil.which", return_value=None):
|
|
self.assertIsNone(get_focused_window())
|
|
|
|
def test_is_focused_window_terminal_false_when_unknown(self):
|
|
with patch(
|
|
"vocalinux.text_injection.focused_window.get_focused_window",
|
|
return_value=None,
|
|
):
|
|
self.assertFalse(is_focused_window_terminal())
|