v1.5.5: terminal zoom, font fallback, copy feedback
- Ctrl+wheel/Ctrl±/Ctrl+0 font zoom (6-28), persisted in settings.json - Font fallback: Cascadia Mono → Consolas → Courier New (per platform) - Visual "Copied!" flash in status bar on copy - Closes audit items #27 (copy feedback) and #29 (font fallback) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,7 @@ class ServerStore:
|
|||||||
self._statuses_lock = threading.Lock()
|
self._statuses_lock = threading.Lock()
|
||||||
self._file_lock = threading.Lock()
|
self._file_lock = threading.Lock()
|
||||||
self._last_backup_time: float = 0
|
self._last_backup_time: float = 0
|
||||||
|
self._terminal_font_size: int = 11
|
||||||
self._servers_file: str = DEFAULT_SERVERS_FILE
|
self._servers_file: str = DEFAULT_SERVERS_FILE
|
||||||
self._load_settings()
|
self._load_settings()
|
||||||
self._load()
|
self._load()
|
||||||
@@ -68,6 +69,7 @@ class ServerStore:
|
|||||||
lang = settings.get("language", "en")
|
lang = settings.get("language", "en")
|
||||||
i18n.set_language(lang)
|
i18n.set_language(lang)
|
||||||
self._check_interval = settings.get("check_interval", 60)
|
self._check_interval = settings.get("check_interval", 60)
|
||||||
|
self._terminal_font_size = settings.get("terminal_font_size", 11)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
log.warning("Corrupted settings.json, using defaults")
|
log.warning("Corrupted settings.json, using defaults")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -80,6 +82,7 @@ class ServerStore:
|
|||||||
"servers_path": self._servers_file,
|
"servers_path": self._servers_file,
|
||||||
"language": i18n.get_language(),
|
"language": i18n.get_language(),
|
||||||
"check_interval": self._check_interval,
|
"check_interval": self._check_interval,
|
||||||
|
"terminal_font_size": self._terminal_font_size,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
tmp = SETTINGS_FILE + ".tmp"
|
tmp = SETTINGS_FILE + ".tmp"
|
||||||
@@ -301,3 +304,12 @@ class ServerStore:
|
|||||||
def get_status(self, alias: str) -> str:
|
def get_status(self, alias: str) -> str:
|
||||||
with self._statuses_lock:
|
with self._statuses_lock:
|
||||||
return self._statuses.get(alias, "unknown")
|
return self._statuses.get(alias, "unknown")
|
||||||
|
|
||||||
|
# ── Terminal font size ────────────────────────────────
|
||||||
|
|
||||||
|
def get_terminal_font_size(self) -> int:
|
||||||
|
return self._terminal_font_size
|
||||||
|
|
||||||
|
def set_terminal_font_size(self, size: int):
|
||||||
|
self._terminal_font_size = max(6, min(28, size))
|
||||||
|
self._save_settings()
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class TerminalTab(ctk.CTkFrame):
|
|||||||
self,
|
self,
|
||||||
send_callback=self._send_to_shell,
|
send_callback=self._send_to_shell,
|
||||||
resize_callback=self._on_resize,
|
resize_callback=self._on_resize,
|
||||||
|
font_size=store.get_terminal_font_size(),
|
||||||
|
on_font_size_changed=self._on_font_size_changed,
|
||||||
)
|
)
|
||||||
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")
|
||||||
@@ -145,3 +147,6 @@ class TerminalTab(ctk.CTkFrame):
|
|||||||
session = self._session # local ref for thread safety
|
session = self._session # local ref for thread safety
|
||||||
if session and session.connected:
|
if session and session.connected:
|
||||||
session.resize(cols, rows)
|
session.resize(cols, rows)
|
||||||
|
|
||||||
|
def _on_font_size_changed(self, size: int):
|
||||||
|
self.store.set_terminal_font_size(size)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ DECCKM, bracketed paste, mouse tracking, and professional copy/paste UX.
|
|||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
import copy
|
import copy
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import tkinter.font as tkfont
|
import tkinter.font as tkfont
|
||||||
@@ -85,6 +86,28 @@ def _xterm_modifier(state: int) -> int:
|
|||||||
return mod
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
# ── Font fallback ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
_FONT_PREFERENCES = {
|
||||||
|
"win32": ["Cascadia Mono", "Consolas", "Courier New"],
|
||||||
|
"darwin": ["Menlo", "Monaco", "Courier"],
|
||||||
|
"linux": ["DejaVu Sans Mono", "Liberation Mono", "Monospace"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _pick_font_family() -> str:
|
||||||
|
"""Pick the best available monospace font for the current platform."""
|
||||||
|
candidates = _FONT_PREFERENCES.get(sys.platform, _FONT_PREFERENCES["linux"])
|
||||||
|
try:
|
||||||
|
available = set(tkfont.families())
|
||||||
|
for name in candidates:
|
||||||
|
if name in available:
|
||||||
|
return name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Ultimate fallback
|
||||||
|
return "Consolas" if sys.platform == "win32" else "Courier"
|
||||||
|
|
||||||
|
|
||||||
# ── Alternate screen buffer ────────────────────────────────────────────────
|
# ── Alternate screen buffer ────────────────────────────────────────────────
|
||||||
|
|
||||||
class _Screen(pyte.Screen):
|
class _Screen(pyte.Screen):
|
||||||
@@ -146,10 +169,12 @@ class TerminalWidget(tk.Frame):
|
|||||||
"""Full-featured VT100/xterm terminal emulator widget."""
|
"""Full-featured VT100/xterm terminal emulator widget."""
|
||||||
|
|
||||||
def __init__(self, master, cols=80, rows=24, send_callback=None,
|
def __init__(self, master, cols=80, rows=24, send_callback=None,
|
||||||
resize_callback=None, **kwargs):
|
resize_callback=None, font_size=None,
|
||||||
|
on_font_size_changed=None, **kwargs):
|
||||||
super().__init__(master, bg=_DEFAULT_BG, **kwargs)
|
super().__init__(master, bg=_DEFAULT_BG, **kwargs)
|
||||||
self.send_callback = send_callback
|
self.send_callback = send_callback
|
||||||
self.resize_callback = resize_callback
|
self.resize_callback = resize_callback
|
||||||
|
self.on_font_size_changed = on_font_size_changed
|
||||||
self._cols = cols
|
self._cols = cols
|
||||||
self._rows = rows
|
self._rows = rows
|
||||||
|
|
||||||
@@ -171,9 +196,13 @@ class TerminalWidget(tk.Frame):
|
|||||||
# Dynamic color tag cache
|
# Dynamic color tag cache
|
||||||
self._dynamic_tags: dict[str, bool] = {}
|
self._dynamic_tags: dict[str, bool] = {}
|
||||||
|
|
||||||
# Fonts
|
# Fonts (with fallback + zoom support)
|
||||||
self._font = tkfont.Font(family="Consolas", size=11)
|
self._font_family = _pick_font_family()
|
||||||
self._bold_font = tkfont.Font(family="Consolas", size=11, weight="bold")
|
self._default_font_size = 11
|
||||||
|
self._font_size = max(6, min(28, font_size or self._default_font_size))
|
||||||
|
self._font = tkfont.Font(family=self._font_family, size=self._font_size)
|
||||||
|
self._bold_font = tkfont.Font(family=self._font_family, size=self._font_size,
|
||||||
|
weight="bold")
|
||||||
|
|
||||||
# Text widget
|
# Text widget
|
||||||
self._text = tk.Text(
|
self._text = tk.Text(
|
||||||
@@ -204,6 +233,14 @@ class TerminalWidget(tk.Frame):
|
|||||||
self._text.bind("<MouseWheel>", self._on_mousewheel)
|
self._text.bind("<MouseWheel>", self._on_mousewheel)
|
||||||
self._text.bind("<Double-Button-1>", self._on_double_click)
|
self._text.bind("<Double-Button-1>", self._on_double_click)
|
||||||
|
|
||||||
|
# ── Zoom bindings (Ctrl+Plus/Minus/0, numpad) ──
|
||||||
|
self._text.bind("<Control-plus>", self._on_zoom_in)
|
||||||
|
self._text.bind("<Control-equal>", self._on_zoom_in)
|
||||||
|
self._text.bind("<Control-minus>", self._on_zoom_out)
|
||||||
|
self._text.bind("<Control-0>", self._on_zoom_reset)
|
||||||
|
self._text.bind("<Control-KP_Add>", self._on_zoom_in)
|
||||||
|
self._text.bind("<Control-KP_Subtract>", self._on_zoom_out)
|
||||||
|
|
||||||
# ── Resize ──
|
# ── Resize ──
|
||||||
self._resize_after_id = None
|
self._resize_after_id = None
|
||||||
self._text.bind("<Configure>", self._on_configure)
|
self._text.bind("<Configure>", self._on_configure)
|
||||||
@@ -212,13 +249,18 @@ class TerminalWidget(tk.Frame):
|
|||||||
self._selecting = False
|
self._selecting = False
|
||||||
self._last_ctrl_c: float = 0.0
|
self._last_ctrl_c: float = 0.0
|
||||||
|
|
||||||
|
# ── Flash status state ──
|
||||||
|
self._flash_after_id = None
|
||||||
|
self._saved_status_text = ""
|
||||||
|
self._saved_status_color = "#888888"
|
||||||
|
|
||||||
# ── Status bar ──
|
# ── Status bar ──
|
||||||
self._status_frame = tk.Frame(self, bg="#2d2d44", height=22)
|
self._status_frame = tk.Frame(self, bg="#2d2d44", height=22)
|
||||||
self._status_frame.pack(fill="x", side="bottom")
|
self._status_frame.pack(fill="x", side="bottom")
|
||||||
self._status_frame.pack_propagate(False)
|
self._status_frame.pack_propagate(False)
|
||||||
self._status_label = tk.Label(
|
self._status_label = tk.Label(
|
||||||
self._status_frame, text="", fg="#888888", bg="#2d2d44",
|
self._status_frame, text="", fg="#888888", bg="#2d2d44",
|
||||||
font=("Consolas", 9), anchor="w", padx=6,
|
font=(self._font_family, 9), anchor="w", padx=6,
|
||||||
)
|
)
|
||||||
self._status_label.pack(fill="x")
|
self._status_label.pack(fill="x")
|
||||||
|
|
||||||
@@ -230,7 +272,8 @@ class TerminalWidget(tk.Frame):
|
|||||||
self._text.tag_configure(f"bg_{name}", background=color)
|
self._text.tag_configure(f"bg_{name}", background=color)
|
||||||
self._text.tag_configure("bold", font=self._bold_font)
|
self._text.tag_configure("bold", font=self._bold_font)
|
||||||
self._text.tag_configure("italic",
|
self._text.tag_configure("italic",
|
||||||
font=tkfont.Font(family="Consolas", size=11, slant="italic"))
|
font=tkfont.Font(family=self._font_family, size=self._font_size,
|
||||||
|
slant="italic"))
|
||||||
self._text.tag_configure("underline", underline=True)
|
self._text.tag_configure("underline", underline=True)
|
||||||
self._text.tag_configure("strikethrough", overstrike=True)
|
self._text.tag_configure("strikethrough", overstrike=True)
|
||||||
self._text.tag_configure("cursor", foreground=_CURSOR_FG,
|
self._text.tag_configure("cursor", foreground=_CURSOR_FG,
|
||||||
@@ -243,6 +286,54 @@ class TerminalWidget(tk.Frame):
|
|||||||
|
|
||||||
def set_status(self, text: str, color: str = "#888888"):
|
def set_status(self, text: str, color: str = "#888888"):
|
||||||
self._status_label.configure(text=text, fg=color)
|
self._status_label.configure(text=text, fg=color)
|
||||||
|
self._saved_status_text = text
|
||||||
|
self._saved_status_color = color
|
||||||
|
|
||||||
|
def _flash_status(self, text: str, color: str, duration: int = 1000):
|
||||||
|
"""Briefly show a flash message in the status bar, then restore."""
|
||||||
|
if self._flash_after_id:
|
||||||
|
self.after_cancel(self._flash_after_id)
|
||||||
|
self._status_label.configure(text=text, fg=color)
|
||||||
|
self._flash_after_id = self.after(duration, self._restore_status)
|
||||||
|
|
||||||
|
def _restore_status(self):
|
||||||
|
self._flash_after_id = None
|
||||||
|
self._status_label.configure(text=self._saved_status_text,
|
||||||
|
fg=self._saved_status_color)
|
||||||
|
|
||||||
|
# ── Zoom ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _set_font_size(self, size: int):
|
||||||
|
size = max(6, min(28, size))
|
||||||
|
if size == self._font_size:
|
||||||
|
return
|
||||||
|
self._font_size = size
|
||||||
|
self._font.configure(size=size)
|
||||||
|
self._bold_font.configure(size=size)
|
||||||
|
# Rebuild italic tag with new size
|
||||||
|
self._text.tag_configure("italic",
|
||||||
|
font=tkfont.Font(family=self._font_family, size=size,
|
||||||
|
slant="italic"))
|
||||||
|
# Force full re-render
|
||||||
|
self._prev_buffer.clear()
|
||||||
|
self._render()
|
||||||
|
# Recalculate cols/rows for new font metrics
|
||||||
|
self._do_resize()
|
||||||
|
# Notify for persistence
|
||||||
|
if self.on_font_size_changed:
|
||||||
|
self.on_font_size_changed(size)
|
||||||
|
|
||||||
|
def _on_zoom_in(self, event=None):
|
||||||
|
self._set_font_size(self._font_size + 1)
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def _on_zoom_out(self, event=None):
|
||||||
|
self._set_font_size(self._font_size - 1)
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def _on_zoom_reset(self, event=None):
|
||||||
|
self._set_font_size(self._default_font_size)
|
||||||
|
return "break"
|
||||||
|
|
||||||
def feed(self, data: bytes):
|
def feed(self, data: bytes):
|
||||||
"""Feed raw bytes from SSH into pyte, schedule debounced render."""
|
"""Feed raw bytes from SSH into pyte, schedule debounced render."""
|
||||||
@@ -423,6 +514,13 @@ class TerminalWidget(tk.Frame):
|
|||||||
# ── Ctrl+key ──
|
# ── Ctrl+key ──
|
||||||
if ctrl:
|
if ctrl:
|
||||||
ks = event.keysym.lower()
|
ks = event.keysym.lower()
|
||||||
|
# Zoom keys — intercept, don't send to SSH
|
||||||
|
if event.keysym in ("plus", "equal", "KP_Add"):
|
||||||
|
self._on_zoom_in(); return "break"
|
||||||
|
if event.keysym in ("minus", "KP_Subtract"):
|
||||||
|
self._on_zoom_out(); return "break"
|
||||||
|
if event.keysym == "0":
|
||||||
|
self._on_zoom_reset(); return "break"
|
||||||
# Handled by dedicated bindings
|
# Handled by dedicated bindings
|
||||||
if ks in ("c", "v", "d", "l", "z"):
|
if ks in ("c", "v", "d", "l", "z"):
|
||||||
# Ctrl+Shift+C/V → copy/paste (handled separately)
|
# Ctrl+Shift+C/V → copy/paste (handled separately)
|
||||||
@@ -537,6 +635,7 @@ class TerminalWidget(tk.Frame):
|
|||||||
if sel:
|
if sel:
|
||||||
self.clipboard_clear()
|
self.clipboard_clear()
|
||||||
self.clipboard_append(sel)
|
self.clipboard_append(sel)
|
||||||
|
self._flash_status("Copied!", "#44cc44")
|
||||||
return "break"
|
return "break"
|
||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
pass
|
pass
|
||||||
@@ -552,6 +651,7 @@ class TerminalWidget(tk.Frame):
|
|||||||
if sel:
|
if sel:
|
||||||
self.clipboard_clear()
|
self.clipboard_clear()
|
||||||
self.clipboard_append(sel)
|
self.clipboard_append(sel)
|
||||||
|
self._flash_status("Copied!", "#44cc44")
|
||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
pass
|
pass
|
||||||
return "break"
|
return "break"
|
||||||
@@ -678,6 +778,14 @@ class TerminalWidget(tk.Frame):
|
|||||||
self._text.configure(state="disabled")
|
self._text.configure(state="disabled")
|
||||||
|
|
||||||
def _on_mousewheel(self, event):
|
def _on_mousewheel(self, event):
|
||||||
|
# Ctrl+wheel → zoom
|
||||||
|
if event.state & 0x4:
|
||||||
|
if event.delta > 0:
|
||||||
|
self._on_zoom_in()
|
||||||
|
else:
|
||||||
|
self._on_zoom_out()
|
||||||
|
return "break"
|
||||||
|
|
||||||
if self._mouse_tracking_active():
|
if self._mouse_tracking_active():
|
||||||
col, row = self._pixel_to_cell(event.x, event.y)
|
col, row = self._pixel_to_cell(event.x, event.y)
|
||||||
# Mouse wheel: button 64 (up) or 65 (down)
|
# Mouse wheel: button 64 (up) or 65 (down)
|
||||||
|
|||||||
BIN
releases/ServerManager-v1.5.5-win-x64.exe
Normal file
BIN
releases/ServerManager-v1.5.5-win-x64.exe
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
"""Version info for ServerManager."""
|
"""Version info for ServerManager."""
|
||||||
|
|
||||||
__version__ = "1.5.4"
|
__version__ = "1.5.5"
|
||||||
__app_name__ = "ServerManager"
|
__app_name__ = "ServerManager"
|
||||||
__author__ = "aibot777"
|
__author__ = "aibot777"
|
||||||
__description__ = "Desktop GUI for managing remote servers"
|
__description__ = "Desktop GUI for managing remote servers"
|
||||||
|
|||||||
Reference in New Issue
Block a user