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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user