fix: improve terminal clipboard interactions
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# Repository guidance
|
||||
|
||||
## Project overview
|
||||
|
||||
WebSSH is a Python 3.10+ web SSH client. Tornado serves the HTTP and WebSocket
|
||||
endpoints, Paramiko manages SSH connections, and xterm.js renders the terminal
|
||||
in the browser.
|
||||
|
||||
## Repository layout
|
||||
|
||||
- `webssh/main.py`: application entry point and route wiring.
|
||||
- `webssh/handler.py`: HTTP/WebSocket handlers and SSH connection setup.
|
||||
- `webssh/worker.py`: asynchronous relay between Tornado and Paramiko channels.
|
||||
- `webssh/settings.py`: CLI options and application/server configuration.
|
||||
- `webssh/auth.py`, `webssh/storage.py`, `webssh/crypto.py`: authentication and
|
||||
persistent saved-connection data.
|
||||
- `webssh/templates/` and `webssh/static/`: browser UI.
|
||||
- `tests/`: pytest/unittest test suite; `tests/sshserver.py` provides the local
|
||||
Paramiko-based integration server.
|
||||
|
||||
## Setup and development commands
|
||||
|
||||
Use Python 3.10, 3.11, or 3.12.
|
||||
|
||||
```bash
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -e .
|
||||
python run.py # listens on 127.0.0.1:8888
|
||||
python run.py --port=8888 --debug=True --data-dir=./data
|
||||
```
|
||||
|
||||
Install test tools with `python -m pip install pytest pytest-cov ruff`. Before
|
||||
handing off a change, run the checks relevant to it:
|
||||
|
||||
```bash
|
||||
python -m pytest tests
|
||||
python -m pytest tests/test_app.py # focused example
|
||||
python -m pytest --cov=webssh
|
||||
ruff check .
|
||||
```
|
||||
|
||||
The legacy Flake8 configuration in `setup.cfg` uses a maximum line length of
|
||||
79 and excludes tests and `__init__.py` files.
|
||||
|
||||
## Implementation constraints
|
||||
|
||||
- Do not block Tornado's IOLoop. Paramiko connection work is blocking and must
|
||||
run through the existing handler `ThreadPoolExecutor` pattern.
|
||||
- Preserve locking around auth data, saved connections, and host-key mutation;
|
||||
these objects are accessed from multiple threads.
|
||||
- New HTTP endpoints should include `MixinHandler`. Protect private endpoints
|
||||
with `@tornado.web.authenticated` and retain the existing origin, XSRF, IP,
|
||||
and session checks.
|
||||
- Treat credentials, private keys, cookie secrets, and saved connection data as
|
||||
sensitive. Never log or expose them to the browser. Persistent auth and
|
||||
connection files must retain mode `0o600`.
|
||||
- Keep saved-password encryption compatible with the cookie secret and do not
|
||||
silently reuse a credential when host, port, or username changes.
|
||||
- Keep changes compatible with all supported Python versions and avoid adding
|
||||
dependencies unless the task requires them.
|
||||
|
||||
## Testing expectations
|
||||
|
||||
- Add or update focused tests for behavior changes and regressions.
|
||||
- Use the local mock SSH server for SSH authentication and connection flows;
|
||||
tests must not depend on external SSH hosts or network services.
|
||||
- For frontend or WebSocket changes, cover both the server handler behavior and
|
||||
the corresponding browser-side message contract when applicable.
|
||||
- Do not commit generated runtime state such as `auth.json`, `connections.json`,
|
||||
cookie secrets, coverage output, caches, or local data directories.
|
||||
+37
-16
@@ -470,8 +470,9 @@ jQuery(function($){
|
||||
function read_clipboard_text(on_text, on_error) {
|
||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||
navigator.clipboard.readText().then(on_text, on_error);
|
||||
return true;
|
||||
} else {
|
||||
on_error();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,24 +856,36 @@ jQuery(function($){
|
||||
return false;
|
||||
}
|
||||
|
||||
copy_text_to_clipboard(term.getSelection(), function(copied) {
|
||||
var selection = term.getSelection();
|
||||
copy_text_to_clipboard(selection, function(copied) {
|
||||
if (copied && term) {
|
||||
term.clearSelection();
|
||||
}
|
||||
if (term) {
|
||||
term.focus();
|
||||
}
|
||||
show_terminal_toast(copied ? '已复制' : '复制失败');
|
||||
});
|
||||
term.clearSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
function paste_into_terminal() {
|
||||
read_clipboard_text(
|
||||
function paste_into_terminal(show_unavailable) {
|
||||
var started = read_clipboard_text(
|
||||
function(text) {
|
||||
if (text) {
|
||||
send_data(text);
|
||||
if (text && term) {
|
||||
// Let xterm normalize newlines and honor bracketed-paste mode.
|
||||
term.paste(text);
|
||||
term.focus();
|
||||
}
|
||||
},
|
||||
function() {
|
||||
show_terminal_toast('无法读取剪贴板,请使用 Ctrl+V 粘贴');
|
||||
show_terminal_toast('剪贴板权限被拒绝,请使用 Ctrl+V 粘贴');
|
||||
}
|
||||
);
|
||||
if (!started && show_unavailable !== false) {
|
||||
show_terminal_toast('浏览器不支持直接读取剪贴板,请使用 Ctrl+V');
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
term.attachCustomKeyEventHandler(function(e) {
|
||||
@@ -882,15 +895,19 @@ jQuery(function($){
|
||||
|
||||
var key = e.key ? e.key.toLowerCase() : '';
|
||||
|
||||
// Ctrl+Shift+C / Ctrl+Shift+V: copy and paste shortcuts
|
||||
if (e.ctrlKey && e.shiftKey && key === 'c') {
|
||||
// Ctrl+Shift+C / Cmd+C: copy the active terminal selection.
|
||||
if (((e.ctrlKey && e.shiftKey) || e.metaKey) && key === 'c' &&
|
||||
term.hasSelection()) {
|
||||
e.preventDefault();
|
||||
copy_terminal_selection();
|
||||
return false;
|
||||
}
|
||||
if (e.ctrlKey && e.shiftKey && key === 'v') {
|
||||
e.preventDefault();
|
||||
paste_into_terminal();
|
||||
|
||||
// Do not let xterm translate Ctrl/Cmd+V into a control character. By
|
||||
// returning false without preventDefault, the browser can dispatch its
|
||||
// trusted paste event to xterm's hidden textarea. This works without the
|
||||
// async Clipboard API and therefore also works on plain HTTP deployments.
|
||||
if ((e.ctrlKey || e.metaKey) && !e.altKey && key === 'v') {
|
||||
return false;
|
||||
}
|
||||
// Ctrl+C with an active selection copies instead of sending SIGINT
|
||||
@@ -903,16 +920,20 @@ jQuery(function($){
|
||||
return true;
|
||||
});
|
||||
|
||||
// Right click: copy the selection if there is one, otherwise paste
|
||||
// Right click: copy the selection if there is one, otherwise paste. When
|
||||
// programmatic clipboard reads are unavailable, keep the native context
|
||||
// menu so the browser's Paste command remains usable.
|
||||
$('#terminal').off('contextmenu.wssh').on('contextmenu.wssh', function(e) {
|
||||
if (!term) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (term.hasSelection()) {
|
||||
e.preventDefault();
|
||||
copy_terminal_selection();
|
||||
} else if (paste_into_terminal(false)) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
paste_into_terminal();
|
||||
show_terminal_toast('请从右键菜单选择粘贴,或使用 Ctrl+V');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user