|
| 1 | +"""Set and monitor clipboard content""" |
| 2 | +import os |
| 3 | +import logging |
| 4 | +from shutil import which |
| 5 | +from lnxlink.modules.scripts.helpers import syscommand |
| 6 | + |
| 7 | +logger = logging.getLogger("lnxlink") |
| 8 | + |
| 9 | + |
| 10 | +class Addon: |
| 11 | + """Addon module""" |
| 12 | + |
| 13 | + def __init__(self, lnxlink): |
| 14 | + """Setup addon""" |
| 15 | + self.name = "Clipboard" |
| 16 | + self.lnxlink = lnxlink |
| 17 | + self.session_type = os.environ.get("XDG_SESSION_TYPE", "unknown").lower() |
| 18 | + self.clipboard_tool = None |
| 19 | + self.last_clipboard = "" |
| 20 | + |
| 21 | + # Add settings with monitoring disabled by default |
| 22 | + self.lnxlink.add_settings( |
| 23 | + "clipboard", |
| 24 | + { |
| 25 | + "monitor_enabled": False, |
| 26 | + }, |
| 27 | + ) |
| 28 | + |
| 29 | + # Determine which clipboard tool to use |
| 30 | + if self.session_type == "wayland": |
| 31 | + if which("wl-copy") is not None and which("wl-paste") is not None: |
| 32 | + self.clipboard_tool = "wl-clipboard" |
| 33 | + logger.debug("Using wl-clipboard for Wayland") |
| 34 | + else: |
| 35 | + raise SystemError( |
| 36 | + "System command 'wl-clipboard' (wl-copy/wl-paste) not found" |
| 37 | + ) |
| 38 | + elif self.session_type == "x11": |
| 39 | + if which("xclip") is not None: |
| 40 | + self.clipboard_tool = "xclip" |
| 41 | + logger.debug("Using xclip for X11") |
| 42 | + elif which("xsel") is not None: |
| 43 | + self.clipboard_tool = "xsel" |
| 44 | + logger.debug("Using xsel for X11") |
| 45 | + else: |
| 46 | + raise SystemError("System command 'xclip' or 'xsel' not found") |
| 47 | + else: |
| 48 | + raise SystemError(f"Unsupported session type: {self.session_type}") |
| 49 | + |
| 50 | + def exposed_controls(self): |
| 51 | + """Exposes to home assistant""" |
| 52 | + discovery_info = { |
| 53 | + "Clipboard Set": { |
| 54 | + "type": "text", |
| 55 | + "icon": "mdi:clipboard-text", |
| 56 | + "max": 255, |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + # Only add monitor sensor if enabled in settings |
| 61 | + if self.lnxlink.config["settings"]["clipboard"]["monitor_enabled"]: |
| 62 | + discovery_info["Clipboard Monitor"] = { |
| 63 | + "type": "sensor", |
| 64 | + "icon": "mdi:clipboard-check", |
| 65 | + "entity_category": "diagnostic", |
| 66 | + } |
| 67 | + |
| 68 | + return discovery_info |
| 69 | + |
| 70 | + def get_info(self): |
| 71 | + """Gather information from the system""" |
| 72 | + # Only monitor clipboard if enabled in settings |
| 73 | + if not self.lnxlink.config["settings"]["clipboard"]["monitor_enabled"]: |
| 74 | + return None |
| 75 | + |
| 76 | + try: |
| 77 | + current_clipboard = self._get_clipboard() |
| 78 | + |
| 79 | + # Only update if clipboard content changed |
| 80 | + if current_clipboard != self.last_clipboard: |
| 81 | + self.last_clipboard = current_clipboard |
| 82 | + return current_clipboard |
| 83 | + except Exception as err: |
| 84 | + logger.debug("Error reading clipboard: %s", err) |
| 85 | + |
| 86 | + return self.last_clipboard |
| 87 | + |
| 88 | + def start_control(self, topic, data): |
| 89 | + """Control system""" |
| 90 | + if topic[1] == "clipboard_set": |
| 91 | + try: |
| 92 | + self._set_clipboard(data) |
| 93 | + logger.info("Clipboard set successfully") |
| 94 | + except Exception as err: |
| 95 | + logger.error("Error setting clipboard: %s", err) |
| 96 | + |
| 97 | + def _get_clipboard(self): |
| 98 | + """Get current clipboard content""" |
| 99 | + if self.clipboard_tool == "wl-clipboard": |
| 100 | + # Use wl-paste for Wayland |
| 101 | + stdout, _, returncode = syscommand( |
| 102 | + "wl-paste --no-newline", |
| 103 | + ignore_errors=True, |
| 104 | + timeout=1, |
| 105 | + ) |
| 106 | + if returncode == 0: |
| 107 | + return stdout |
| 108 | + |
| 109 | + elif self.clipboard_tool == "xclip": |
| 110 | + # Use xclip for X11 |
| 111 | + stdout, _, returncode = syscommand( |
| 112 | + "xclip -selection clipboard -o", |
| 113 | + ignore_errors=True, |
| 114 | + timeout=1, |
| 115 | + ) |
| 116 | + if returncode == 0: |
| 117 | + return stdout |
| 118 | + |
| 119 | + elif self.clipboard_tool == "xsel": |
| 120 | + # Use xsel for X11 |
| 121 | + stdout, _, returncode = syscommand( |
| 122 | + "xsel --clipboard --output", |
| 123 | + ignore_errors=True, |
| 124 | + timeout=1, |
| 125 | + ) |
| 126 | + if returncode == 0: |
| 127 | + return stdout |
| 128 | + |
| 129 | + return "" |
| 130 | + |
| 131 | + def _set_clipboard(self, text): |
| 132 | + """Set clipboard content""" |
| 133 | + # Escape single quotes in text for shell command |
| 134 | + escaped_text = text.replace("'", "'\\''") |
| 135 | + |
| 136 | + if self.clipboard_tool == "wl-clipboard": |
| 137 | + # Use wl-copy for Wayland |
| 138 | + _, _, returncode = syscommand( |
| 139 | + f"printf '%s' '{escaped_text}' | wl-copy", |
| 140 | + timeout=2, |
| 141 | + ) |
| 142 | + if returncode != 0: |
| 143 | + raise SystemError(f"wl-copy failed with code {returncode}") |
| 144 | + |
| 145 | + elif self.clipboard_tool == "xclip": |
| 146 | + # Use xclip for X11 |
| 147 | + _, _, returncode = syscommand( |
| 148 | + f"printf '%s' '{escaped_text}' | xclip -selection clipboard", |
| 149 | + timeout=2, |
| 150 | + ) |
| 151 | + if returncode != 0: |
| 152 | + raise SystemError(f"xclip failed with code {returncode}") |
| 153 | + |
| 154 | + elif self.clipboard_tool == "xsel": |
| 155 | + # Use xsel for X11 |
| 156 | + _, _, returncode = syscommand( |
| 157 | + f"printf '%s' '{escaped_text}' | xsel --clipboard --input", |
| 158 | + timeout=2, |
| 159 | + ) |
| 160 | + if returncode != 0: |
| 161 | + raise SystemError(f"xsel failed with code {returncode}") |
| 162 | + |
| 163 | + # Update last_clipboard to avoid triggering monitor on self-set |
| 164 | + self.last_clipboard = text |
0 commit comments