From 80cd3ad5db58fd593dcaa7d0160e2633ea165ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Thu, 21 May 2026 11:09:48 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20nuclear=20paste=20fix=20=E2=80=94=20bind?= =?UTF-8?q?=20all=20macOS=20modifier=20variants=20for=20Cmd+V/C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- ssh_manager.py | 65 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/ssh_manager.py b/ssh_manager.py index dd091fa..7d81f88 100644 --- a/ssh_manager.py +++ b/ssh_manager.py @@ -493,8 +493,7 @@ def agent_search(query): def setup_entry_clipboard(entry): - """Fix clipboard on macOS (Tk 8.6 maps <> to Mod1 not Command) - and add right-click context menu.""" + """Fix clipboard on all platforms. Covers every known Tk modifier mapping.""" def _paste(e=None): try: @@ -526,23 +525,43 @@ def setup_entry_clipboard(entry): def _context_menu(event): menu = tk.Menu(entry, tearoff=0, bg="#313244", fg="#cdd6f4", activebackground="#45475a") - menu.add_command(label="Вырезать", command=_cut) - menu.add_command(label="Копировать", command=_copy) - menu.add_command(label="Вставить", command=_paste) + menu.add_command(label="Cut", command=_cut) + menu.add_command(label="Copy", command=_copy) + menu.add_command(label="Paste", command=_paste) menu.add_separator() - menu.add_command(label="Выделить всё", command=_select_all) + menu.add_command(label="Select All", command=_select_all) menu.tk_popup(event.x_root, event.y_root) + # Bind virtual events (works on all platforms) + entry.bind("<>", _paste) + entry.bind("<>", _copy) + entry.bind("<>", _cut) + entry.bind("<>", _select_all) + if sys.platform == "darwin": - # macOS Tk 8.6 bug: <> bound to Mod1 (Option) not Command - # Manually bind Command+key for clipboard operations - entry.bind("", _paste) - entry.bind("", _copy) - entry.bind("", _cut) - entry.bind("", _select_all) + # macOS: physical Command key may map to Command, Mod1, Mod2, or Meta + # depending on Tk version and frozen/unfrozen state. + # Bind ALL possible modifiers to guarantee it works. + for mod in ("Command", "Mod1", "Meta", "Mod2"): + entry.bind(f"<{mod}-v>", _paste) + entry.bind(f"<{mod}-c>", _copy) + entry.bind(f"<{mod}-x>", _cut) + entry.bind(f"<{mod}-a>", _select_all) + # Also register Command-v as trigger for <> virtual event + for seq in ("", "", "", ""): + try: + entry.event_add("<>", seq) + except tk.TclError: + pass # Right-click entry.bind("", _context_menu) entry.bind("", _context_menu) + else: + # Windows/Linux: Ctrl+V + entry.bind("", _paste) + entry.bind("", _copy) + entry.bind("", _cut) + entry.bind("", _select_all) entry.bind("", _context_menu) @@ -763,9 +782,25 @@ class TerminalWidget(tk.Frame): self.text.bind("", lambda e: self._send("\x0c")) self.text.bind("", lambda e: self._send("\x1a")) # Paste/Copy in terminal: send clipboard to SSH / copy selection + # Bind virtual events first + self.text.bind("<>", self._term_paste) + self.text.bind("<>", self._term_copy) if sys.platform == "darwin": - self.text.bind("", self._term_paste) - self.text.bind("", self._term_copy) + # Nuclear: bind ALL possible macOS modifier mappings + for mod in ("Command", "Mod1", "Meta", "Mod2"): + self.text.bind(f"<{mod}-v>", self._term_paste) + self.text.bind(f"<{mod}-c>", self._term_copy) + # Also register as virtual event triggers + for seq in ("", "", "", ""): + try: + self.text.event_add("<>", seq) + except tk.TclError: + pass + for seq in ("", "", "", ""): + try: + self.text.event_add("<>", seq) + except tk.TclError: + pass else: # Windows/Linux: Ctrl+V paste, Ctrl+Shift+C copy self.text.bind("", self._term_paste) @@ -887,6 +922,8 @@ class TerminalWidget(tk.Frame): return if event.state & 8: # Mod1 / Command on macOS — let bindings handle it return + if sys.platform == "darwin" and (event.state & 0x10): # Mod2 — Command on some Tk builds + return if event.char and ord(event.char) >= 32: self._send(event.char) return "break"