xshell-like 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xshell_like-0.2.1/xshell_like.egg-info → xshell_like-0.2.2}/PKG-INFO +1 -1
- {xshell_like-0.2.1 → xshell_like-0.2.2}/pyproject.toml +1 -1
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_terminal.py +46 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/__main__.py +5 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/terminal.py +30 -4
- {xshell_like-0.2.1 → xshell_like-0.2.2/xshell_like.egg-info}/PKG-INFO +1 -1
- {xshell_like-0.2.1 → xshell_like-0.2.2}/LICENSE +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/README.md +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/setup.cfg +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_batch.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_gui_smoke.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_keydeploy.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_serial_ports.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_serial_transport.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_session_storage.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_ssh_transport.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/tests/test_terminal_widget.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/__init__.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/__init__.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/batch.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/keydeploy.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/serial_ports.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/serial_transport.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/session.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/sftp.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/ssh_transport.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/storage.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/core/transport.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/py.typed +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/ui/__init__.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/ui/dialogs.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/ui/main_window.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/ui/session_tree.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/ui/sftp_dialog.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/ui/terminal_widget.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell/ui/theme.py +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell_like.egg-info/SOURCES.txt +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell_like.egg-info/dependency_links.txt +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell_like.egg-info/entry_points.txt +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell_like.egg-info/requires.txt +0 -0
- {xshell_like-0.2.1 → xshell_like-0.2.2}/xshell_like.egg-info/top_level.txt +0 -0
|
@@ -139,3 +139,49 @@ def test_cursor_follows_ansi_cursor_move():
|
|
|
139
139
|
assert tb.cursor == (0, 1)
|
|
140
140
|
tb.feed(b"\x1b[1A") # CUU: up one row, column unchanged → row 0, col 0
|
|
141
141
|
assert tb.cursor == (0, 0)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------- resize anchoring (xterm semantics) ----------
|
|
145
|
+
|
|
146
|
+
def test_grow_keeps_content_at_top_blanks_below():
|
|
147
|
+
tb = TerminalBuffer(cols=20, rows=6)
|
|
148
|
+
tb.feed(b"line1\r\nline2\r\nline3\r\nline4")
|
|
149
|
+
tb.resize(20, 12)
|
|
150
|
+
lines = [ln.rstrip() for ln in tb.render().splitlines()]
|
|
151
|
+
assert lines[0] == "line1" and lines[3] == "line4"
|
|
152
|
+
assert all(not ln for ln in lines[4:])
|
|
153
|
+
assert tb.cursor == (5, 3)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_shrink_keeps_cursor_line_visible():
|
|
157
|
+
"""Regression: pyte's own shrink truncates the bottom and drops the prompt
|
|
158
|
+
line — after SIGWINCH the remote repainted it mid-screen with blank space
|
|
159
|
+
above. xterm semantics: the cursor line must stay visible."""
|
|
160
|
+
tb = TerminalBuffer(cols=20, rows=8)
|
|
161
|
+
tb.feed(b"l1\r\nl2\r\nl3\r\nprompt$ ")
|
|
162
|
+
tb.resize(20, 3)
|
|
163
|
+
lines = [ln.rstrip() for ln in tb.render().splitlines()]
|
|
164
|
+
assert lines[-1].startswith("prompt$") # cursor line kept at the bottom
|
|
165
|
+
assert len(lines) == 3
|
|
166
|
+
assert tb.cursor[1] <= 2 # cursor clamped into the shrunken screen
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_shrink_then_grow_no_blank_gap_above_content():
|
|
170
|
+
tb = TerminalBuffer(cols=20, rows=8)
|
|
171
|
+
tb.feed(b"l1\r\nl2\r\nl3\r\nprompt$ ")
|
|
172
|
+
tb.resize(20, 3) # user drags the window smaller
|
|
173
|
+
tb.resize(20, 8) # ...then bigger again
|
|
174
|
+
lines = [ln.rstrip() for ln in tb.render().splitlines()]
|
|
175
|
+
assert lines[0] == "l2" # l1 went to scrollback, content anchored to top
|
|
176
|
+
assert lines[2].startswith("prompt$")
|
|
177
|
+
assert all(not ln for ln in lines[3:]) # blanks BELOW content, never above
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_shrink_drops_top_lines_into_scrollback():
|
|
181
|
+
tb = TerminalBuffer(cols=20, rows=6)
|
|
182
|
+
tb.feed(b"a\r\nb\r\nc\r\nd\r\ne\r\nf\r\nprompt$ ") # cursor on row 6
|
|
183
|
+
tb.resize(20, 4) # cursor line must survive -> 3 top lines to scrollback
|
|
184
|
+
sb = tb.scrollback
|
|
185
|
+
assert "a" in sb and "b" in sb and "c" in sb
|
|
186
|
+
lines = [ln.rstrip() for ln in tb.render().splitlines()]
|
|
187
|
+
assert lines[-1].startswith("prompt$")
|
|
@@ -3,12 +3,17 @@ import sys
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def main() -> int:
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
6
7
|
from PySide6.QtWidgets import QApplication
|
|
7
8
|
|
|
8
9
|
from xshell.core.storage import SessionStore
|
|
9
10
|
from xshell.ui.main_window import MainWindow
|
|
10
11
|
from xshell.ui.theme import apply_theme
|
|
11
12
|
|
|
13
|
+
# honor the display's fractional scaling exactly (HiDPI / 4K screens)
|
|
14
|
+
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
|
15
|
+
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
|
16
|
+
)
|
|
12
17
|
app = QApplication(sys.argv)
|
|
13
18
|
app.setApplicationName("xshell-like")
|
|
14
19
|
app.setOrganizationName("xshell-like")
|
|
@@ -69,8 +69,34 @@ class TerminalBuffer:
|
|
|
69
69
|
rows = max(2, int(rows))
|
|
70
70
|
if cols == self.cols and rows == self.rows:
|
|
71
71
|
return
|
|
72
|
-
|
|
73
|
-
self.screen.
|
|
72
|
+
old_rows = self.rows
|
|
73
|
+
buffer = self.screen.buffer
|
|
74
|
+
cursor_y = min(self.screen.cursor.y, old_rows - 1)
|
|
75
|
+
|
|
76
|
+
# Line dimension is handled HERE, with xterm semantics: content is
|
|
77
|
+
# top-anchored and a shrink drops lines from the TOP into scrollback
|
|
78
|
+
# so the cursor's line (the prompt!) stays visible. pyte's own resize
|
|
79
|
+
# keeps cursor.y absolute instead — after a shrink the prompt line is
|
|
80
|
+
# dropped and the remote's SIGWINCH repaint lands mid-screen with
|
|
81
|
+
# blank space above it ("content doesn't fill the window").
|
|
82
|
+
drop = max(0, cursor_y - (rows - 1)) if rows < old_rows else 0
|
|
83
|
+
for y in range(drop):
|
|
84
|
+
row = buffer.get(y)
|
|
85
|
+
if row is not None:
|
|
86
|
+
self.screen.history.top.append(row)
|
|
87
|
+
old_rows_list = [buffer.get(y) for y in range(old_rows)]
|
|
88
|
+
buffer.clear()
|
|
89
|
+
for new_y in range(rows):
|
|
90
|
+
src = old_rows_list[drop + new_y] if drop + new_y < old_rows else None
|
|
91
|
+
if src is not None:
|
|
92
|
+
buffer[new_y] = src
|
|
93
|
+
self.screen.cursor.y = max(0, min(cursor_y - drop, rows - 1))
|
|
94
|
+
self.screen.lines = rows
|
|
95
|
+
|
|
96
|
+
# Column dimension: pyte truncates/pads the right side fine. With
|
|
97
|
+
# lines already equal, its resize only runs the column logic.
|
|
98
|
+
pyte.Screen.resize(self.screen, rows, cols)
|
|
99
|
+
self.screen.dirty = set(range(rows))
|
|
74
100
|
|
|
75
101
|
# ---------- output ----------
|
|
76
102
|
|
|
@@ -109,7 +135,7 @@ class TerminalBuffer:
|
|
|
109
135
|
current_style = style
|
|
110
136
|
run.append(char.data if char.data else " ")
|
|
111
137
|
parts.append(self._wrap("".join(run), current_style))
|
|
112
|
-
return "".join(parts).rstrip("
|
|
138
|
+
return "".join(parts).rstrip(" ") or " "
|
|
113
139
|
|
|
114
140
|
@staticmethod
|
|
115
141
|
def _char_style(char: Char) -> Optional[str]:
|
|
@@ -144,7 +170,7 @@ class TerminalBuffer:
|
|
|
144
170
|
text.replace("&", "&")
|
|
145
171
|
.replace("<", "<")
|
|
146
172
|
.replace(">", ">")
|
|
147
|
-
.replace(" ", "
|
|
173
|
+
.replace(" ", " ")
|
|
148
174
|
)
|
|
149
175
|
if not style:
|
|
150
176
|
return text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|