Fix DECCKM support for arrow keys in TUI apps (mc, vim, htop)
- mc/vim/htop enable Application Cursor Mode (DECCKM ?1h) which requires arrow keys to send \eOA..D instead of \e[A..D - Detect DECCKM in pyte screen mode and send correct sequences - Fix thread-safety: use queue.Queue for SSH→main thread data transfer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import customtkinter as ctk
|
|||||||
from core.ssh_client import ShellSession
|
from core.ssh_client import ShellSession
|
||||||
from core.i18n import t
|
from core.i18n import t
|
||||||
|
|
||||||
|
import queue
|
||||||
|
|
||||||
|
|
||||||
class TerminalTab(ctk.CTkFrame):
|
class TerminalTab(ctk.CTkFrame):
|
||||||
def __init__(self, master, store):
|
def __init__(self, master, store):
|
||||||
@@ -30,8 +32,8 @@ class TerminalTab(ctk.CTkFrame):
|
|||||||
self._terminal.pack(fill="both", expand=True, padx=5, pady=5)
|
self._terminal.pack(fill="both", expand=True, padx=5, pady=5)
|
||||||
self._terminal.set_status(t("term_disconnected"), "#888888")
|
self._terminal.set_status(t("term_disconnected"), "#888888")
|
||||||
|
|
||||||
# Data batching buffer
|
# Thread-safe data batching
|
||||||
self._data_buffer = bytearray()
|
self._data_queue: queue.Queue[bytes] = queue.Queue()
|
||||||
self._flush_after_id = None
|
self._flush_after_id = None
|
||||||
|
|
||||||
def set_server(self, alias: str | None):
|
def set_server(self, alias: str | None):
|
||||||
@@ -88,16 +90,20 @@ class TerminalTab(ctk.CTkFrame):
|
|||||||
self._session = None
|
self._session = None
|
||||||
|
|
||||||
def _on_data_received(self, data: bytes):
|
def _on_data_received(self, data: bytes):
|
||||||
self._data_buffer.extend(data)
|
"""Called from SSH thread — put data in thread-safe queue."""
|
||||||
if self._flush_after_id is None:
|
self._data_queue.put(data)
|
||||||
self._flush_after_id = self.after(8, self._flush_data_buffer)
|
self.after(0, self._flush_data_queue)
|
||||||
|
|
||||||
def _flush_data_buffer(self):
|
def _flush_data_queue(self):
|
||||||
self._flush_after_id = None
|
"""Called on main thread — drain queue and feed terminal."""
|
||||||
if self._data_buffer:
|
chunks = []
|
||||||
chunk = bytes(self._data_buffer)
|
try:
|
||||||
self._data_buffer.clear()
|
while True:
|
||||||
self._terminal.feed(chunk)
|
chunks.append(self._data_queue.get_nowait())
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
if chunks:
|
||||||
|
self._terminal.feed(b"".join(chunks))
|
||||||
|
|
||||||
def _on_disconnected(self):
|
def _on_disconnected(self):
|
||||||
if self._intentional_disconnect:
|
if self._intentional_disconnect:
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ _DEFAULT_BG = "#1a1a2e"
|
|||||||
_CURSOR_FG = "#1a1a2e"
|
_CURSOR_FG = "#1a1a2e"
|
||||||
_CURSOR_BG = "#d3d7cf"
|
_CURSOR_BG = "#d3d7cf"
|
||||||
|
|
||||||
# Key → VT100 escape sequence
|
# DECCKM (private mode ?1) — pyte stores private modes shifted by 5 bits
|
||||||
|
_DECCKM = 32
|
||||||
|
|
||||||
|
# Key → VT100 escape sequence (normal cursor mode)
|
||||||
_KEY_MAP = {
|
_KEY_MAP = {
|
||||||
"Up": "\x1b[A",
|
"Up": "\x1b[A",
|
||||||
"Down": "\x1b[B",
|
"Down": "\x1b[B",
|
||||||
@@ -59,6 +62,16 @@ _KEY_MAP = {
|
|||||||
"F12": "\x1b[24~",
|
"F12": "\x1b[24~",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Application cursor mode (DECCKM) — mc, htop, vim enable this
|
||||||
|
_KEY_MAP_APP_CURSOR = {
|
||||||
|
"Up": "\x1bOA",
|
||||||
|
"Down": "\x1bOB",
|
||||||
|
"Right": "\x1bOC",
|
||||||
|
"Left": "\x1bOD",
|
||||||
|
"Home": "\x1bOH",
|
||||||
|
"End": "\x1bOF",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TerminalWidget(tk.Frame):
|
class TerminalWidget(tk.Frame):
|
||||||
"""VT100 terminal emulator widget using pyte + tkinter.Text."""
|
"""VT100 terminal emulator widget using pyte + tkinter.Text."""
|
||||||
@@ -301,8 +314,12 @@ class TerminalWidget(tk.Frame):
|
|||||||
self._send(bytes([ord(ch) - ord("a") + 1]))
|
self._send(bytes([ord(ch) - ord("a") + 1]))
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
# Special keys
|
# Special keys — check DECCKM (application cursor mode) for arrow keys
|
||||||
if event.keysym in _KEY_MAP:
|
if event.keysym in _KEY_MAP:
|
||||||
|
decckm = _DECCKM in self._screen.mode
|
||||||
|
if decckm and event.keysym in _KEY_MAP_APP_CURSOR:
|
||||||
|
self._send(_KEY_MAP_APP_CURSOR[event.keysym].encode())
|
||||||
|
else:
|
||||||
self._send(_KEY_MAP[event.keysym].encode())
|
self._send(_KEY_MAP[event.keysym].encode())
|
||||||
return "break"
|
return "break"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user