Add ability for X clibpard and Fenrir's clipboard to stay in sync.
This commit is contained in:
@@ -22,7 +22,7 @@ class command:
|
||||
self.env["runtime"]["MemoryManager"].add_index_list(
|
||||
"clipboardHistory",
|
||||
self.env["runtime"]["SettingsManager"].get_setting_as_int(
|
||||
"general", "number_of_clipboards"
|
||||
"clipboard", "number_of_clipboards"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class command:
|
||||
def run(self):
|
||||
clipboard_file_path = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting("general", "clipboard_export_path")
|
||||
].get_setting("clipboard", "clipboard_export_path")
|
||||
clipboard_file_path = clipboard_file_path.replace(
|
||||
"$user", self.env["general"]["curr_user"]
|
||||
)
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import _thread
|
||||
import importlib
|
||||
import os
|
||||
|
||||
import pyperclip
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
from fenrirscreenreader.utils import x_clipboard
|
||||
|
||||
|
||||
class command:
|
||||
@@ -46,36 +43,20 @@ class command:
|
||||
"MemoryManager"
|
||||
].get_index_list_element("clipboardHistory")
|
||||
|
||||
# Remember original display environment variable if it exists
|
||||
original_display = os.environ.get("DISPLAY", "")
|
||||
success = False
|
||||
|
||||
# Try different display options
|
||||
for i in range(10):
|
||||
display = f":{i}"
|
||||
try:
|
||||
# Set display environment variable
|
||||
os.environ["DISPLAY"] = display
|
||||
# Attempt to set clipboard content
|
||||
# Weird workaround for some distros
|
||||
importlib.reload(pyperclip)
|
||||
pyperclip.copy(clipboard)
|
||||
# If we get here without exception, we found a working
|
||||
# display
|
||||
success = True
|
||||
break
|
||||
except Exception:
|
||||
# Failed for this display, try next one
|
||||
continue
|
||||
|
||||
# Restore original display setting
|
||||
if original_display:
|
||||
os.environ["DISPLAY"] = original_display
|
||||
else:
|
||||
os.environ.pop("DISPLAY", None)
|
||||
try:
|
||||
success = x_clipboard.write_text(
|
||||
clipboard, scan_displays=True
|
||||
)
|
||||
except Exception:
|
||||
success = False
|
||||
|
||||
# Notify the user of the result
|
||||
if success:
|
||||
sync_manager = self.env["runtime"].get(
|
||||
"ClipboardSyncManager"
|
||||
)
|
||||
if sync_manager:
|
||||
sync_manager.mark_written_to_x(clipboard)
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
_("exported to the X session."), interrupt=True
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ class command:
|
||||
def run(self):
|
||||
clipboard_file_path = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting("general", "clipboard_export_path")
|
||||
].get_setting("clipboard", "clipboard_export_path")
|
||||
clipboard_file_path = clipboard_file_path.replace(
|
||||
"$user", self.env["general"]["curr_user"]
|
||||
)
|
||||
|
||||
@@ -5,12 +5,9 @@
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import _thread
|
||||
import importlib
|
||||
import os
|
||||
|
||||
import pyperclip
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
from fenrirscreenreader.utils import x_clipboard
|
||||
|
||||
|
||||
class command:
|
||||
@@ -32,33 +29,12 @@ class command:
|
||||
|
||||
def _thread_run(self):
|
||||
try:
|
||||
# Remember original display environment variable if it exists
|
||||
original_display = os.environ.get("DISPLAY", "")
|
||||
clipboard_content = None
|
||||
|
||||
# Try different display options
|
||||
for i in range(10):
|
||||
display = f":{i}"
|
||||
try:
|
||||
# Set display environment variable
|
||||
os.environ["DISPLAY"] = display
|
||||
# Attempt to get clipboard content
|
||||
# Weird workaround for some distros
|
||||
importlib.reload(pyperclip)
|
||||
clipboard_content = pyperclip.paste()
|
||||
# If we get here without exception, we found a working
|
||||
# display
|
||||
if clipboard_content:
|
||||
break
|
||||
except Exception:
|
||||
# Failed for this display, try next one
|
||||
continue
|
||||
|
||||
# Restore original display setting
|
||||
if original_display:
|
||||
os.environ["DISPLAY"] = original_display
|
||||
else:
|
||||
os.environ.pop("DISPLAY", None)
|
||||
try:
|
||||
clipboard_content = x_clipboard.read_text(
|
||||
scan_displays=True
|
||||
)
|
||||
except Exception:
|
||||
clipboard_content = None
|
||||
|
||||
# Process the clipboard content if we found any
|
||||
if clipboard_content and isinstance(clipboard_content, str):
|
||||
|
||||
@@ -18,7 +18,7 @@ class command:
|
||||
self.env["runtime"]["MemoryManager"].add_index_list(
|
||||
"clipboardHistory",
|
||||
self.env["runtime"]["SettingsManager"].get_setting_as_int(
|
||||
"general", "number_of_clipboards"
|
||||
"clipboard", "number_of_clipboards"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -141,7 +141,12 @@ class command(config_command):
|
||||
self.config["general"] = {
|
||||
"punctuation_level": "some",
|
||||
"debug_level": "0",
|
||||
}
|
||||
|
||||
# Basic clipboard defaults
|
||||
self.config["clipboard"] = {
|
||||
"number_of_clipboards": "50",
|
||||
"clipboard_export_path": "/tmp/fenrirClipboard",
|
||||
}
|
||||
|
||||
# Write the configuration
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.utils import x_clipboard
|
||||
|
||||
|
||||
class ClipboardSyncManager:
|
||||
def __init__(self):
|
||||
self.env = None
|
||||
self.enabled = False
|
||||
self.display = ""
|
||||
self.interval = 0.5
|
||||
self.running = False
|
||||
self.thread = None
|
||||
self.last_written_to_x = None
|
||||
self.last_imported_from_x = None
|
||||
self.last_observed_fenrir = None
|
||||
self.last_observed_x = None
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
self.enabled, self.display = self._resolve_startup()
|
||||
self.interval = max(
|
||||
0.1,
|
||||
self.env["runtime"]["SettingsManager"].get_setting_as_float(
|
||||
"clipboard", "sync_interval"
|
||||
),
|
||||
)
|
||||
if self.enabled:
|
||||
self._debug(
|
||||
"Clipboard sync enabled for display "
|
||||
+ self.display
|
||||
+ " at interval "
|
||||
+ str(self.interval)
|
||||
)
|
||||
self.start()
|
||||
else:
|
||||
self._debug("Clipboard sync disabled at startup")
|
||||
|
||||
def shutdown(self):
|
||||
self.stop()
|
||||
|
||||
def mark_written_to_x(self, text):
|
||||
if not isinstance(text, str) or not text:
|
||||
return
|
||||
self.last_written_to_x = text
|
||||
self.last_observed_fenrir = text
|
||||
self.last_observed_x = text
|
||||
|
||||
def _resolve_startup(self):
|
||||
settings_manager = self.env["runtime"]["SettingsManager"]
|
||||
if not settings_manager.get_setting_as_bool(
|
||||
"clipboard", "sync_enabled"
|
||||
):
|
||||
return False, ""
|
||||
|
||||
configured_display = settings_manager.get_setting(
|
||||
"clipboard", "sync_display"
|
||||
)
|
||||
screen_driver = settings_manager.get_setting("screen", "driver")
|
||||
keyboard_driver = settings_manager.get_setting("keyboard", "driver")
|
||||
is_x_mode = (
|
||||
screen_driver == "ptyDriver" and keyboard_driver == "x11Driver"
|
||||
)
|
||||
|
||||
if configured_display:
|
||||
return True, configured_display
|
||||
if is_x_mode:
|
||||
current_display = os.environ.get("DISPLAY", "")
|
||||
return bool(current_display), current_display
|
||||
return False, ""
|
||||
|
||||
def start(self):
|
||||
if self.running:
|
||||
return
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._run, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=1.0)
|
||||
self.thread = None
|
||||
|
||||
def _run(self):
|
||||
while self.running:
|
||||
self.poll_once()
|
||||
time.sleep(self.interval)
|
||||
|
||||
def poll_once(self):
|
||||
fenrir_text = self._get_fenrir_clipboard_text()
|
||||
x_text = self._get_x_clipboard_text()
|
||||
|
||||
if fenrir_text and x_text and fenrir_text == x_text:
|
||||
self.last_observed_fenrir = fenrir_text
|
||||
self.last_observed_x = x_text
|
||||
return
|
||||
|
||||
fenrir_changed = (
|
||||
fenrir_text
|
||||
and fenrir_text != self.last_observed_fenrir
|
||||
)
|
||||
x_changed = (
|
||||
x_text
|
||||
and x_text != self.last_observed_x
|
||||
)
|
||||
|
||||
if fenrir_changed:
|
||||
if self._write_x_clipboard_text(fenrir_text):
|
||||
self.last_written_to_x = fenrir_text
|
||||
self.last_observed_x = fenrir_text
|
||||
self.last_observed_fenrir = fenrir_text
|
||||
return
|
||||
|
||||
if x_changed:
|
||||
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
|
||||
"clipboardHistory", x_text
|
||||
)
|
||||
self.last_imported_from_x = x_text
|
||||
self.last_observed_fenrir = x_text
|
||||
self.last_observed_x = x_text
|
||||
return
|
||||
|
||||
if fenrir_text:
|
||||
self.last_observed_fenrir = fenrir_text
|
||||
if x_text:
|
||||
self.last_observed_x = x_text
|
||||
|
||||
def _get_fenrir_clipboard_text(self):
|
||||
memory_manager = self.env["runtime"]["MemoryManager"]
|
||||
if memory_manager.is_index_list_empty("clipboardHistory"):
|
||||
return None
|
||||
text = memory_manager.get_index_list_element("clipboardHistory")
|
||||
if isinstance(text, str) and text:
|
||||
return text
|
||||
return None
|
||||
|
||||
def _get_x_clipboard_text(self):
|
||||
try:
|
||||
text = x_clipboard.read_text(self.display)
|
||||
if isinstance(text, str) and text:
|
||||
return text
|
||||
return None
|
||||
except Exception as error:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"ClipboardSyncManager paste failed: " + str(error),
|
||||
debug.DebugLevel.INFO,
|
||||
)
|
||||
return None
|
||||
|
||||
def _write_x_clipboard_text(self, text):
|
||||
try:
|
||||
return x_clipboard.write_text(text, self.display)
|
||||
except Exception as error:
|
||||
self._debug("ClipboardSyncManager copy failed: " + str(error))
|
||||
return False
|
||||
|
||||
def _debug(self, message):
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
message,
|
||||
debug.DebugLevel.INFO,
|
||||
)
|
||||
@@ -17,6 +17,7 @@ general_data = {
|
||||
"CursorManager",
|
||||
"ApplicationManager",
|
||||
"CommandManager",
|
||||
"ClipboardSyncManager",
|
||||
"ScreenManager",
|
||||
"InputManager",
|
||||
"OutputManager",
|
||||
|
||||
@@ -422,7 +422,7 @@ class RemoteManager:
|
||||
def export_clipboard(self):
|
||||
clipboard_file_path = self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting("general", "clipboard_export_path")
|
||||
].get_setting("clipboard", "clipboard_export_path")
|
||||
clipboard_file_path = clipboard_file_path.replace(
|
||||
"$user", self.env["general"]["curr_user"]
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ runtime_data = {
|
||||
"RemoteDriver": None,
|
||||
"InputManager": None,
|
||||
"CommandManager": None,
|
||||
"ClipboardSyncManager": None,
|
||||
"ScreenManager": None,
|
||||
"OutputManager": None,
|
||||
"SpeechHistoryManager": None,
|
||||
|
||||
@@ -70,8 +70,6 @@ settings_data = {
|
||||
"respect_punctuation_pause": True,
|
||||
"replace_undefined_punctuation_with_space": True,
|
||||
"new_line_pause": True,
|
||||
"number_of_clipboards": 10,
|
||||
"clipboard_export_path": "/tmp/fenrirClipboard",
|
||||
"emoticons": True,
|
||||
"fenrir_keys": "KEY_KP0,KEY_META",
|
||||
"script_keys": "KEY_COMPOSE",
|
||||
@@ -89,6 +87,13 @@ settings_data = {
|
||||
"diff_presentation": "both",
|
||||
"diff_verbosity": "compact",
|
||||
},
|
||||
"clipboard": {
|
||||
"number_of_clipboards": 10,
|
||||
"clipboard_export_path": "/tmp/fenrirClipboard",
|
||||
"sync_enabled": False,
|
||||
"sync_display": "",
|
||||
"sync_interval": 0.5,
|
||||
},
|
||||
"focus": {
|
||||
"cursor": True,
|
||||
"highlight": False,
|
||||
|
||||
@@ -12,6 +12,7 @@ from configparser import ConfigParser
|
||||
from fenrirscreenreader.core import applicationManager
|
||||
from fenrirscreenreader.core import attributeManager
|
||||
from fenrirscreenreader.core import barrierManager
|
||||
from fenrirscreenreader.core import clipboardSyncManager
|
||||
from fenrirscreenreader.core import commandManager
|
||||
from fenrirscreenreader.core import cursorManager
|
||||
from fenrirscreenreader.core import debug
|
||||
@@ -771,6 +772,13 @@ class SettingsManager:
|
||||
] = commandManager.CommandManager()
|
||||
environment["runtime"]["CommandManager"].initialize(environment)
|
||||
|
||||
environment["runtime"][
|
||||
"ClipboardSyncManager"
|
||||
] = clipboardSyncManager.ClipboardSyncManager()
|
||||
environment["runtime"]["ClipboardSyncManager"].initialize(
|
||||
environment
|
||||
)
|
||||
|
||||
environment["runtime"]["HelpManager"] = helpManager.HelpManager()
|
||||
environment["runtime"]["HelpManager"].initialize(environment)
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2026.06.18"
|
||||
version = "2026.06.27"
|
||||
code_name = "testing"
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
|
||||
_display_lock = threading.RLock()
|
||||
_CLIPBOARD_TIMEOUT = 2.0
|
||||
_ENCODING = "utf-8"
|
||||
|
||||
|
||||
def _command_exists(command):
|
||||
return shutil.which(command) is not None
|
||||
|
||||
|
||||
def _env_for_display(display):
|
||||
env = os.environ.copy()
|
||||
if display:
|
||||
env["DISPLAY"] = display
|
||||
return env
|
||||
|
||||
|
||||
def _display_candidates(display="", scan_displays=False):
|
||||
if display:
|
||||
return [display]
|
||||
current_display = os.environ.get("DISPLAY", "")
|
||||
candidates = []
|
||||
if current_display:
|
||||
candidates.append(current_display)
|
||||
if scan_displays:
|
||||
candidates.extend(f":{index}" for index in range(10))
|
||||
if not candidates:
|
||||
candidates.append("")
|
||||
return list(dict.fromkeys(candidates))
|
||||
|
||||
|
||||
def _read_commands(display):
|
||||
commands = []
|
||||
if display:
|
||||
if _command_exists("xclip"):
|
||||
commands.append(["xclip", "-selection", "clipboard", "-o"])
|
||||
if _command_exists("xsel"):
|
||||
commands.append(["xsel", "-b", "-o"])
|
||||
elif os.environ.get("WAYLAND_DISPLAY") and _command_exists("wl-paste"):
|
||||
commands.append(["wl-paste", "-n", "-t", "text"])
|
||||
return commands
|
||||
|
||||
|
||||
def _write_commands(display):
|
||||
commands = []
|
||||
if display:
|
||||
if _command_exists("xclip"):
|
||||
commands.append(["xclip", "-selection", "clipboard"])
|
||||
if _command_exists("xsel"):
|
||||
commands.append(["xsel", "-b", "-i"])
|
||||
elif os.environ.get("WAYLAND_DISPLAY") and _command_exists("wl-copy"):
|
||||
commands.append(["wl-copy"])
|
||||
return commands
|
||||
|
||||
|
||||
def _run_command(command, display, input_text=None):
|
||||
input_bytes = None
|
||||
if input_text is not None:
|
||||
input_bytes = input_text.encode(_ENCODING)
|
||||
return subprocess.run(
|
||||
command,
|
||||
input=input_bytes,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=_env_for_display(display),
|
||||
timeout=_CLIPBOARD_TIMEOUT,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _command_error(command, result):
|
||||
stderr = result.stderr.decode(_ENCODING, errors="replace").strip()
|
||||
if stderr:
|
||||
return RuntimeError(stderr)
|
||||
return RuntimeError(
|
||||
"clipboard command failed: "
|
||||
+ " ".join(command)
|
||||
+ " exited with "
|
||||
+ str(result.returncode)
|
||||
)
|
||||
|
||||
|
||||
def read_text(display="", scan_displays=False):
|
||||
last_error = None
|
||||
command_succeeded = False
|
||||
with _display_lock:
|
||||
for candidate in _display_candidates(display, scan_displays):
|
||||
commands = _read_commands(candidate)
|
||||
if not commands:
|
||||
last_error = RuntimeError("no supported clipboard reader found")
|
||||
for command in commands:
|
||||
try:
|
||||
result = _run_command(command, candidate)
|
||||
if result.returncode != 0:
|
||||
last_error = _command_error(command, result)
|
||||
continue
|
||||
command_succeeded = True
|
||||
text = result.stdout.decode(_ENCODING)
|
||||
if text:
|
||||
return text
|
||||
except Exception as error:
|
||||
last_error = error
|
||||
if last_error:
|
||||
if command_succeeded:
|
||||
return None
|
||||
raise last_error
|
||||
return None
|
||||
|
||||
|
||||
def write_text(text, display="", scan_displays=False):
|
||||
if not isinstance(text, str) or not text:
|
||||
return False
|
||||
last_error = None
|
||||
with _display_lock:
|
||||
for candidate in _display_candidates(display, scan_displays):
|
||||
commands = _write_commands(candidate)
|
||||
if not commands:
|
||||
last_error = RuntimeError("no supported clipboard writer found")
|
||||
for command in commands:
|
||||
try:
|
||||
result = _run_command(command, candidate, text)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
last_error = _command_error(command, result)
|
||||
except Exception as error:
|
||||
last_error = error
|
||||
if last_error:
|
||||
raise last_error
|
||||
return False
|
||||
Reference in New Issue
Block a user