v1.8.52: icons module, Windows SSH sanitization, embedded RDP improvements, UI polish

- Add core/icons.py — centralized icon text helper with emoji/symbol support
- Add Windows SSH command sanitization in ssh.py (Linux→Windows auto-translation)
- Improve embedded RDP: launch tab connect/disconnect, fullscreen toggle
- Refactor sidebar: cleaner server type badges
- Update server_dialog: adaptive fields per server type
- Add setup_openssh.bat tool
- Update skill-ssh.md and CLAUDE.md docs for Windows SSH support
- Cleanup old releases, add v1.8.48-v1.8.52

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-24 14:37:37 -05:00
parent 142b68515c
commit 4959004a3f
30 changed files with 596 additions and 134 deletions

View File

@@ -250,8 +250,12 @@ class EmbeddedRDP:
self.on_failed = None # called on embed failure
self.on_disconnected = None # called when mstsc exits
def generate_rdp_file(self, width: int, height: int) -> str:
"""Build a .rdp temp file with all settings."""
def generate_rdp_file(self, window_w: int, window_h: int) -> str:
"""Build a .rdp temp file with all settings.
window_w/window_h: physical frame size for embedding.
Session resolution comes from settings["resolution"] (e.g. "1920x1080" or "auto").
"""
s = self.server
cfg = self.settings
hostname = s["ip"]
@@ -259,6 +263,17 @@ class EmbeddedRDP:
user = s.get("user", "Administrator")
password = s.get("password", "")
# Session resolution.
# "auto": use frame size — session matches the embed area exactly.
# Fixed (e.g. "1920x1080"): use that resolution, smart sizing scales
# the content to fit the frame.
resolution = cfg.get("resolution", "auto")
if resolution != "auto":
parts = resolution.split("x")
desk_w, desk_h = int(parts[0]), int(parts[1])
else:
desk_w, desk_h = window_w, window_h
quality = cfg.get("quality", "auto")
conn_type, bpp, no_wallpaper, no_themes, font_smooth, aero = _QUALITY_PRESETS.get(quality, _QUALITY_PRESETS["auto"])
@@ -269,10 +284,10 @@ class EmbeddedRDP:
lines = [
f"full address:s:{hostname}:{port}",
f"username:s:{user}",
f"desktopwidth:i:{width}",
f"desktopheight:i:{height}",
f"desktopwidth:i:{desk_w}",
f"desktopheight:i:{desk_h}",
"screen mode id:i:1", # windowed (required for embedding)
"smart sizing:i:1", # scale to window
"use multimon:i:0",
f"session bpp:i:{bpp}",
f"connection type:i:{conn_type}",
"compression:i:1",
@@ -297,6 +312,10 @@ class EmbeddedRDP:
"enablerdsaadauth:i:0",
]
# smart sizing scales the fixed-resolution bitmap to fit the mstsc window.
# No dynamic resolution — session resolution is set once at connect time.
lines.append("smart sizing:i:1")
if drives:
lines.append(f"drivestoredirect:s:{drives}")
@@ -314,8 +333,12 @@ class EmbeddedRDP:
log.info(f"Embedded RDP file: {self._rdp_file}")
return self._rdp_file
def launch(self, parent_hwnd: int, width: int = 1024, height: int = 768):
"""Launch mstsc and start background embed thread."""
def launch(self, parent_hwnd: int, window_w: int = 1024, window_h: int = 768):
"""Launch mstsc and start background embed thread.
window_w/window_h: physical size of the embedding frame.
Session resolution is set via settings["resolution"] in the .rdp file.
"""
self._parent_hwnd = parent_hwnd
# Pre-trust server certificate to suppress dialog
@@ -323,16 +346,17 @@ class EmbeddedRDP:
port = self.server.get("port", 3389)
_trust_rdp_server(hostname, port)
rdp_file = self.generate_rdp_file(width, height)
rdp_file = self.generate_rdp_file(window_w, window_h)
self._launch_time = time.time()
# Always launch mstsc at frame size — smart sizing scales the session
self._process = subprocess.Popen(
["mstsc.exe", rdp_file, f"/w:{width}", f"/h:{height}"],
["mstsc.exe", rdp_file, f"/w:{window_w}", f"/h:{window_h}"],
creationflags=0x00000010, # CREATE_NEW_CONSOLE suppressed
)
log.info(f"mstsc.exe launched, PID={self._process.pid}")
threading.Thread(target=self._find_and_embed, args=(parent_hwnd, width, height), daemon=True).start()
threading.Thread(target=self._find_and_embed, args=(parent_hwnd, window_w, window_h), daemon=True).start()
def _find_and_embed(self, parent_hwnd: int, width: int, height: int):
"""Background: poll for mstsc window using multi-strategy search, then embed.
@@ -567,10 +591,13 @@ class EmbeddedRDP:
# Resize to fill parent
user32.MoveWindow(hwnd, 0, 0, width, height, True)
# Apply style change
# Apply style change — recalculates non-client area
user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOZORDER | 0x0001 | 0x0002)
# Re-position after FRAMECHANGED to fix non-client area offset
user32.MoveWindow(hwnd, 0, 0, width, height, True)
# Focus
user32.SetFocus(hwnd)
@@ -593,9 +620,14 @@ class EmbeddedRDP:
"""Resize the embedded mstsc window."""
if not self._mstsc_hwnd or not self._connected:
return
if width < 200 or height < 150:
return # Ignore degenerate sizes
try:
import ctypes
ctypes.windll.user32.MoveWindow(self._mstsc_hwnd, 0, 0, width, height, True)
user32 = ctypes.windll.user32
if not user32.IsWindow(self._mstsc_hwnd):
return
user32.MoveWindow(self._mstsc_hwnd, 0, 0, width, height, True)
except Exception:
pass
@@ -610,7 +642,7 @@ class EmbeddedRDP:
pass
def detach(self):
"""Detach mstsc from parent — for fullscreen."""
"""Detach mstsc from parent (step 1). Call maximize() after a delay."""
if not self._mstsc_hwnd or not self._connected:
return
self._is_detached = True
@@ -619,29 +651,67 @@ class EmbeddedRDP:
import ctypes.wintypes
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
hwnd = self._mstsc_hwnd
# Thread input attachment for cross-process style changes
target_tid = user32.GetWindowThreadProcessId(hwnd, None)
our_tid = kernel32.GetCurrentThreadId()
attached = False
if target_tid != our_tid:
attached = bool(user32.AttachThreadInput(our_tid, target_tid, True))
# Reparent to desktop
user32.SetParent(hwnd, 0)
# Restore normal window style
# Remove WS_CHILD, set normal top-level window style
GWL_STYLE = -16
WS_OVERLAPPEDWINDOW = 0x00CF0000
WS_VISIBLE = 0x10000000
user32.SetWindowLongW(hwnd, -16, WS_OVERLAPPEDWINDOW | WS_VISIBLE)
# Maximize
user32.ShowWindow(hwnd, 3) # SW_MAXIMIZE
WS_CHILD = 0x40000000
style = user32.GetWindowLongW(hwnd, GWL_STYLE) & 0xFFFFFFFF
new_style = ((style & ~WS_CHILD) | WS_OVERLAPPEDWINDOW | WS_VISIBLE) & 0xFFFFFFFF
user32.SetWindowLongW(hwnd, GWL_STYLE, ctypes.c_long(new_style).value)
# Apply frame change
SWP_FRAMECHANGED = 0x0020
SWP_NOZORDER = 0x0004
SWP_NOSIZE = 0x0001
SWP_NOMOVE = 0x0002
user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOZORDER | 0x0001 | 0x0002)
user32.SetForegroundWindow(hwnd)
SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOSIZE | SWP_NOMOVE)
log.info("mstsc detached to fullscreen")
if attached:
user32.AttachThreadInput(our_tid, target_tid, False)
log.info("mstsc detached from parent")
except Exception as e:
log.error(f"Detach failed: {e}")
def maximize(self):
"""Maximize the detached mstsc window (step 2, call after delay)."""
if not self._mstsc_hwnd:
return
try:
import ctypes
user32 = ctypes.windll.user32
hwnd = self._mstsc_hwnd
# Bring to foreground
user32.SetForegroundWindow(hwnd)
# Try ShowWindow first
user32.ShowWindow(hwnd, 3) # SW_MAXIMIZE
# Fallback: explicit resize to screen
screen_w = user32.GetSystemMetrics(0)
screen_h = user32.GetSystemMetrics(1)
user32.MoveWindow(hwnd, 0, 0, screen_w, screen_h, True)
log.info(f"mstsc maximize attempted ({screen_w}x{screen_h})")
except Exception as e:
log.error(f"Maximize failed: {e}")
def reattach(self, parent_hwnd: int, width: int, height: int):
"""Re-embed mstsc back into the tkinter frame."""
if not self._mstsc_hwnd: