Improve VS Code terminal panel behaviour
This commit is contained in:
@@ -52,6 +52,11 @@ type Session struct {
|
||||
reconnectCount int // number of reconnection attempts so far
|
||||
cancelReconn context.CancelFunc // cancels an in-progress reconnect backoff
|
||||
|
||||
// Latest terminal geometry received before a PTY-capable client is attached.
|
||||
// The frontend can know the correct size before SSH/Telnet/local setup finishes.
|
||||
pendingResizeCols int
|
||||
pendingResizeRows int
|
||||
|
||||
// Login script support — temporary buffer for expect-style matching.
|
||||
loginBuf []byte
|
||||
loginBufMu sync.Mutex
|
||||
@@ -74,8 +79,8 @@ type Manager struct {
|
||||
hostKeyMu sync.Mutex
|
||||
hostKeyChans map[string]chan bool
|
||||
hostKeyListenerRegistered bool
|
||||
secretMu sync.Mutex
|
||||
secretChans map[string]chan string
|
||||
secretMu sync.Mutex
|
||||
secretChans map[string]chan string
|
||||
}
|
||||
|
||||
// NewManager creates a Manager wired to the given backing services.
|
||||
@@ -270,7 +275,11 @@ func (m *Manager) ConnectLocal(shell string) (string, error) {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
sess.localClient = client
|
||||
cols, rows := sess.consumePendingResizeLocked()
|
||||
m.mu.Unlock()
|
||||
applyLocalResize(client, cols, rows)
|
||||
|
||||
m.mu.Lock()
|
||||
sess.Info.State = models.SessionConnected
|
||||
@@ -373,26 +382,66 @@ func (m *Manager) WriteToSession(sessionID string, data []byte) error {
|
||||
|
||||
// ResizeSession sends a terminal resize to a session.
|
||||
func (m *Manager) ResizeSession(sessionID string, cols, rows int) error {
|
||||
m.mu.RLock()
|
||||
m.mu.Lock()
|
||||
sess, ok := m.sessions[sessionID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("sessions: session %q not found", sessionID)
|
||||
}
|
||||
|
||||
sshClient := sess.sshClient
|
||||
telnetClient := sess.telnetClient
|
||||
localClient := sess.localClient
|
||||
isRaw := sess.rawClient != nil || (sess.profile != nil && sess.profile.Protocol == models.ProtocolRAW)
|
||||
if sshClient == nil && telnetClient == nil && localClient == nil && !isRaw {
|
||||
sess.pendingResizeCols = cols
|
||||
sess.pendingResizeRows = rows
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case sess.sshClient != nil:
|
||||
return sess.sshClient.Resize(cols, rows)
|
||||
case sess.telnetClient != nil:
|
||||
return sess.telnetClient.Resize(cols, rows)
|
||||
case sess.localClient != nil:
|
||||
return sess.localClient.Resize(uint16(cols), uint16(rows))
|
||||
case sshClient != nil:
|
||||
return sshClient.Resize(cols, rows)
|
||||
case telnetClient != nil:
|
||||
return telnetClient.Resize(cols, rows)
|
||||
case localClient != nil:
|
||||
return localClient.Resize(uint16(cols), uint16(rows))
|
||||
}
|
||||
// RAW sessions have no concept of terminal size.
|
||||
return nil
|
||||
}
|
||||
|
||||
// consumePendingResizeLocked returns and clears the latest pre-connect geometry.
|
||||
// m.mu must be held by the caller.
|
||||
func (sess *Session) consumePendingResizeLocked() (int, int) {
|
||||
cols := sess.pendingResizeCols
|
||||
rows := sess.pendingResizeRows
|
||||
sess.pendingResizeCols = 0
|
||||
sess.pendingResizeRows = 0
|
||||
return cols, rows
|
||||
}
|
||||
|
||||
func applySSHResize(client *sshclient.Client, cols, rows int) {
|
||||
if cols <= 0 || rows <= 0 {
|
||||
return
|
||||
}
|
||||
_ = client.Resize(cols, rows)
|
||||
}
|
||||
|
||||
func applyTelnetResize(client *telnetclient.Client, cols, rows int) {
|
||||
if cols <= 0 || rows <= 0 {
|
||||
return
|
||||
}
|
||||
_ = client.Resize(cols, rows)
|
||||
}
|
||||
|
||||
func applyLocalResize(client *localshell.Client, cols, rows int) {
|
||||
if cols <= 0 || rows <= 0 {
|
||||
return
|
||||
}
|
||||
_ = client.Resize(uint16(cols), uint16(rows))
|
||||
}
|
||||
|
||||
// GetSessionCwd returns a best-effort working directory for SSH sessions.
|
||||
func (m *Manager) GetSessionCwd(sessionID string) (string, error) {
|
||||
m.mu.RLock()
|
||||
@@ -750,7 +799,11 @@ func (m *Manager) connectSSH(sess *Session, getSecret func(prompt string) (strin
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
sess.sshClient = client
|
||||
cols, rows := sess.consumePendingResizeLocked()
|
||||
m.mu.Unlock()
|
||||
applySSHResize(client, cols, rows)
|
||||
|
||||
// Start any port-forwarding rules configured in the profile.
|
||||
if sshConf != nil && len(sshConf.PortForwards) > 0 {
|
||||
@@ -849,7 +902,11 @@ func (m *Manager) connectTelnet(sess *Session) error {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
sess.telnetClient = client
|
||||
cols, rows := sess.consumePendingResizeLocked()
|
||||
m.mu.Unlock()
|
||||
applyTelnetResize(client, cols, rows)
|
||||
|
||||
// Execute login scripts if configured.
|
||||
telConf := profile.Telnet
|
||||
|
||||
@@ -29,6 +29,7 @@ done
|
||||
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm.min.js" "$VSCODE_DIR/nexus/media/xterm.min.js"
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-fit.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-fit.min.js"
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-unicode11.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-unicode11.min.js"
|
||||
cp "$ROOT_DIR/frontend/dist/src/css/lib/xterm.css" "$VSCODE_DIR/nexus/media/xterm.css"
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/novnc.min.js" "$VSCODE_DIR/nexus/media/novnc.min.js"
|
||||
|
||||
|
||||
+173
-66
@@ -336,7 +336,7 @@ class ConnectionsProvider {
|
||||
}
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(var(--action-count, 3), minmax(0, 1fr));
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
.actions button {
|
||||
@@ -355,7 +355,6 @@ class ConnectionsProvider {
|
||||
text-align: center;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
.status { color: var(--vscode-descriptionForeground); font-size: 11px; min-height: 15px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -373,7 +372,6 @@ class ConnectionsProvider {
|
||||
<div id="segments" class="segments"></div>
|
||||
<div id="stats" class="stats"></div>
|
||||
<div id="list"></div>
|
||||
<div id="status" class="status"></div>
|
||||
</div>
|
||||
<script nonce="${nonce}">
|
||||
const vscode = acquireVsCodeApi();
|
||||
@@ -381,7 +379,6 @@ class ConnectionsProvider {
|
||||
let filter = 'all';
|
||||
const protocols = ['all', 'ssh', 'telnet', 'raw', 'github', 'vnc', 'irc'];
|
||||
const search = document.getElementById('search');
|
||||
const status = document.getElementById('status');
|
||||
function esc(value) {
|
||||
return String(value == null ? '' : value).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
||||
}
|
||||
@@ -428,10 +425,13 @@ class ConnectionsProvider {
|
||||
}
|
||||
function card(profile) {
|
||||
const tags = (profile.tags || []).slice(0, 4).map(tag => '<span class="tag">' + esc(tag) + '</span>').join('');
|
||||
const sftp = profile.protocol === 'ssh' ? '<button data-action="sftp">SFTP</button>' : '<button data-action="duplicate">Copy</button>';
|
||||
const isSSH = profile.protocol === 'ssh';
|
||||
const actions = isSSH
|
||||
? '<div class="actions" style="--action-count: 4"><button data-action="connect" title="Open SSH terminal">SSH</button><button data-action="sftp" title="Open SFTP browser">SFTP</button><button data-action="edit" title="Edit profile">✏️</button><button data-action="delete" title="Delete profile">🗑️</button></div>'
|
||||
: '<div class="actions"><button data-action="connect">Open</button><button data-action="edit" title="Edit profile">✏️</button><button data-action="delete" title="Delete profile">🗑️</button></div>';
|
||||
return '<article class="card" data-id="' + esc(profile.id) + '">' +
|
||||
'<div class="card-main" data-action="connect"><div class="proto">' + esc(protoLabel(profile.protocol)) + '</div><div><div class="title">' + esc(profile.displayName || profile.host || profile.id) + '</div><div class="meta">' + esc(detail(profile)) + '</div>' + (tags ? '<div class="tags">' + tags + '</div>' : '') + '</div></div>' +
|
||||
'<div class="actions"><button data-action="connect">Open</button><button data-action="edit">Edit</button>' + sftp + '<button data-action="delete">Delete</button></div>' +
|
||||
actions +
|
||||
'</article>';
|
||||
}
|
||||
function render() {
|
||||
@@ -443,7 +443,6 @@ class ConnectionsProvider {
|
||||
if (!visible.length) {
|
||||
root.innerHTML = '<div class="empty">No matching connections<br><br><button class="primary" data-empty-new>New profile</button></div>';
|
||||
root.querySelector('[data-empty-new]').onclick = () => vscode.postMessage({ type: 'new', protocol: 'ssh' });
|
||||
status.textContent = '';
|
||||
return;
|
||||
}
|
||||
const groups = new Map();
|
||||
@@ -457,7 +456,6 @@ class ConnectionsProvider {
|
||||
'<div class="group-title"><span>' + esc(name) + '</span><span>' + items.length + '</span></div>' +
|
||||
items.sort((a, b) => String(a.displayName || a.host).localeCompare(String(b.displayName || b.host))).map(card).join('')
|
||||
).join('');
|
||||
status.textContent = visible.length + ' connection' + (visible.length === 1 ? '' : 's');
|
||||
}
|
||||
document.getElementById('new').onclick = () => vscode.postMessage({ type: 'new', protocol: filter === 'all' ? 'ssh' : filter });
|
||||
document.getElementById('settings').onclick = () => vscode.postMessage({ type: 'settings' });
|
||||
@@ -473,7 +471,7 @@ class ConnectionsProvider {
|
||||
window.addEventListener('message', event => {
|
||||
const msg = event.data || {};
|
||||
if (msg.type === 'profiles') { profiles = msg.profiles || []; render(); }
|
||||
if (msg.type === 'error') status.textContent = msg.error || '';
|
||||
if (msg.type === 'error') console.error(msg.error || '');
|
||||
});
|
||||
vscode.postMessage({ type: 'refresh' });
|
||||
</script>
|
||||
@@ -512,9 +510,19 @@ class TerminalPanel {
|
||||
await this.bridge.rpc('WriteSession', [this.session.id, msg.data]);
|
||||
} else if (msg.type === 'resize') {
|
||||
await this.bridge.rpc('ResizeSession', [this.session.id, msg.cols, msg.rows]);
|
||||
} else if (msg.type === 'reconnect') {
|
||||
const oldSessionID = this.session.id;
|
||||
const protocol = String(this.profile.protocol || this.session.protocol || '').toLowerCase();
|
||||
const session = protocol === 'local'
|
||||
? await this.bridge.rpc('ConnectLocal')
|
||||
: await this.bridge.rpc('ConnectSession', [this.profile.id]);
|
||||
this.session = session;
|
||||
this.panel.title = session.tabName || session.profileName || this.panel.title;
|
||||
this.panel.webview.postMessage({ type: 'state', text: 'connecting' });
|
||||
this.bridge.rpc('DisconnectSession', [oldSessionID]).catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
this.panel.webview.postMessage({ type: 'status', text: err.message });
|
||||
this.panel.webview.postMessage({ type: 'state', text: err.message || String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,10 +532,10 @@ class TerminalPanel {
|
||||
this.panel.webview.postMessage({ type: 'data', data: payload.data });
|
||||
}
|
||||
if (evt.name === 'session:state' && payload.sessionID === this.session.id) {
|
||||
this.panel.webview.postMessage({ type: 'status', text: payload.error || payload.state });
|
||||
this.panel.webview.postMessage({ type: 'state', text: payload.error || payload.state });
|
||||
}
|
||||
if (evt.name === 'session:closed' && payload.sessionID === this.session.id) {
|
||||
this.panel.webview.postMessage({ type: 'status', text: 'disconnected' });
|
||||
this.panel.webview.postMessage({ type: 'state', text: 'disconnected' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,6 +544,7 @@ class TerminalPanel {
|
||||
const mediaRoot = vscode.Uri.file(path.join(this.context.extensionPath, 'media'));
|
||||
const xtermJs = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm.min.js'));
|
||||
const fitJs = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm-addon-fit.min.js'));
|
||||
const unicodeJs = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm-addon-unicode11.min.js'));
|
||||
const xtermCss = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm.css'));
|
||||
const nonce = String(Date.now());
|
||||
const terminalFontFamily = JSON.stringify(this.profile.terminalFontFamily || '');
|
||||
@@ -562,10 +571,24 @@ class TerminalPanel {
|
||||
#terminal {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 4px 6px;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#terminal .xterm {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
#terminal .xterm-screen {
|
||||
height: 100% !important;
|
||||
}
|
||||
#terminal .xterm-viewport {
|
||||
height: 100%;
|
||||
}
|
||||
.xterm { height: 100%; }
|
||||
.xterm-viewport::-webkit-scrollbar { width: 10px; }
|
||||
.xterm-viewport::-webkit-scrollbar-thumb {
|
||||
background: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, .4));
|
||||
@@ -573,42 +596,80 @@ class TerminalPanel {
|
||||
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100, 100, 100, .7));
|
||||
}
|
||||
/* Floating, auto-hiding status pill following VSCode badge styling. */
|
||||
#status {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 10px;
|
||||
z-index: 3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 11px;
|
||||
font: 11px/1.6 var(--vscode-font-family);
|
||||
color: var(--vscode-badge-foreground);
|
||||
background: var(--vscode-badge-background);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
|
||||
body.is-disconnected #terminal {
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
#disconnectOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(10, 14, 18, .68);
|
||||
opacity: 0;
|
||||
transition: opacity .25s ease;
|
||||
visibility: hidden;
|
||||
transition: opacity .16s ease, visibility .16s ease;
|
||||
}
|
||||
#status.show { opacity: 1; }
|
||||
#status .dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--vscode-charts-yellow, #e2c08d);
|
||||
body.is-disconnected #disconnectOverlay {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.disconnect-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: min(360px, calc(100vw - 32px));
|
||||
padding: 18px 24px;
|
||||
text-align: center;
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-editorWidget-background, var(--vscode-panel-background));
|
||||
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-panel-border));
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .32);
|
||||
}
|
||||
.disconnect-title {
|
||||
font: 600 15px/1.35 var(--vscode-font-family);
|
||||
letter-spacing: .45em;
|
||||
text-indent: .45em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#disconnectMessage {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
max-width: 320px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
#reconnect {
|
||||
border: 1px solid var(--vscode-button-border, transparent);
|
||||
padding: 5px 14px;
|
||||
color: var(--vscode-button-foreground);
|
||||
background: var(--vscode-button-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
#reconnect:hover {
|
||||
background: var(--vscode-button-hoverBackground, var(--vscode-button-background));
|
||||
}
|
||||
#reconnect:disabled {
|
||||
cursor: default;
|
||||
opacity: .7;
|
||||
}
|
||||
#status.ok .dot { background: var(--vscode-charts-green, #89d185); }
|
||||
#status.bad { color: var(--vscode-statusBarItem-errorForeground, #fff); background: var(--vscode-statusBarItem-errorBackground, #c72e2e); }
|
||||
#status.bad .dot { background: currentColor; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="status" class="show"><span class="dot"></span><span id="statusText">Connecting…</span></div>
|
||||
<div id="terminal"></div>
|
||||
<div id="disconnectOverlay" aria-hidden="true">
|
||||
<div class="disconnect-box">
|
||||
<div class="disconnect-title">Disconnected</div>
|
||||
<div id="disconnectMessage">Connection closed.</div>
|
||||
<button id="reconnect" type="button">Reconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
<script nonce="${nonce}" src="${xtermJs}"></script>
|
||||
<script nonce="${nonce}" src="${fitJs}"></script>
|
||||
<script nonce="${nonce}" src="${unicodeJs}"></script>
|
||||
<script nonce="${nonce}">
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
@@ -651,12 +712,14 @@ class TerminalPanel {
|
||||
const fontFamily = ${terminalFontFamily}
|
||||
|| cssVar('--vscode-terminal-font-family', '')
|
||||
|| cssVar('--vscode-editor-font-family', 'Menlo, Monaco, "Cascadia Code", "Courier New", monospace');
|
||||
const unicodeFontFallbacks = '"JetBrains Mono", "Cascadia Code", "Fira Code", Menlo, Monaco, Consolas, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Symbols Nerd Font", "PowerlineSymbols", monospace';
|
||||
const resolvedFontFamily = [fontFamily, unicodeFontFallbacks].filter(Boolean).join(', ');
|
||||
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: ${terminalCursorStyle},
|
||||
scrollback: ${terminalScrollback},
|
||||
fontFamily,
|
||||
fontFamily: resolvedFontFamily,
|
||||
fontSize: ${terminalFontSize},
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: 0,
|
||||
@@ -665,16 +728,24 @@ class TerminalPanel {
|
||||
});
|
||||
const fit = new FitAddon.FitAddon();
|
||||
term.loadAddon(fit);
|
||||
try {
|
||||
if (window.Unicode11Addon) {
|
||||
const unicode11 = new Unicode11Addon.Unicode11Addon();
|
||||
term.loadAddon(unicode11);
|
||||
term.unicode.activeVersion = '11';
|
||||
}
|
||||
} catch (_) {
|
||||
// Unicode width support is optional; font fallbacks still preserve glyph rendering.
|
||||
}
|
||||
const container = document.getElementById('terminal');
|
||||
term.open(container);
|
||||
|
||||
let lastCols = 0;
|
||||
let lastRows = 0;
|
||||
// Push the current terminal geometry to the backend PTY. Pass force=true to
|
||||
// re-send even when the dimensions are unchanged (needed right after the
|
||||
// connection completes, since the remote PTY is created at a default size).
|
||||
function resize(force) {
|
||||
try { fit.fit(); } catch (_) { return; }
|
||||
let resizeRaf = 0;
|
||||
|
||||
function sendResize(force) {
|
||||
if (term.cols <= 0 || term.rows <= 0) return;
|
||||
if (force || term.cols !== lastCols || term.rows !== lastRows) {
|
||||
lastCols = term.cols;
|
||||
lastRows = term.rows;
|
||||
@@ -682,6 +753,26 @@ class TerminalPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Push the current terminal geometry to the backend PTY. Pass force=true to
|
||||
// re-send even when the dimensions are unchanged (needed right after the
|
||||
// connection completes, since the remote PTY is created at a default size).
|
||||
function resize(force) {
|
||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||
resizeRaf = requestAnimationFrame(() => {
|
||||
resizeRaf = 0;
|
||||
const rect = container.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) return;
|
||||
try { fit.fit(); } catch (_) { return; }
|
||||
sendResize(force);
|
||||
});
|
||||
}
|
||||
|
||||
function resizeBurst(force) {
|
||||
resize(force);
|
||||
setTimeout(() => resize(force), 60);
|
||||
setTimeout(() => resize(force), 250);
|
||||
}
|
||||
|
||||
// Refit whenever the tab/container changes size — handles splits, panel drags
|
||||
// and the editor being shown after retainContextWhenHidden.
|
||||
if (window.ResizeObserver) {
|
||||
@@ -689,39 +780,55 @@ class TerminalPanel {
|
||||
}
|
||||
window.addEventListener('resize', () => resize());
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) resize(); });
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => resize());
|
||||
requestAnimationFrame(() => resize());
|
||||
setTimeout(() => resize(), 60);
|
||||
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => resizeBurst(true));
|
||||
resizeBurst(true);
|
||||
|
||||
term.focus();
|
||||
term.onData(data => vscode.postMessage({ type: 'input', data: btoa(unescape(encodeURIComponent(data))) }));
|
||||
const encoder = new TextEncoder();
|
||||
function utf8ToBase64(value) {
|
||||
const bytes = encoder.encode(value);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return btoa(binary);
|
||||
}
|
||||
term.onData(data => vscode.postMessage({ type: 'input', data: utf8ToBase64(data) }));
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const statusText = document.getElementById('statusText');
|
||||
let hideTimer = null;
|
||||
function setStatus(text) {
|
||||
const reconnectButton = document.getElementById('reconnect');
|
||||
const disconnectMessage = document.getElementById('disconnectMessage');
|
||||
function setConnectionState(text) {
|
||||
const value = String(text || '').trim();
|
||||
if (!value) return;
|
||||
if (!value) return false;
|
||||
const lower = value.toLowerCase();
|
||||
const connected = /connect(ed)?$|ready|open|online/.test(lower) && !/dis|fail|error|refus|lost|closed/.test(lower);
|
||||
const failed = /dis|fail|error|refus|lost|closed|timeout/.test(lower);
|
||||
statusText.textContent = value.charAt(0).toUpperCase() + value.slice(1);
|
||||
statusEl.classList.toggle('ok', connected && !failed);
|
||||
statusEl.classList.toggle('bad', failed);
|
||||
statusEl.classList.add('show');
|
||||
clearTimeout(hideTimer);
|
||||
// Healthy states fade out so they don't sit on top of the terminal; problems stay visible.
|
||||
if (connected && !failed) hideTimer = setTimeout(() => statusEl.classList.remove('show'), 1600);
|
||||
return connected && !failed;
|
||||
const disconnected = /dis|fail|error|refus|lost|closed|timeout/.test(lower);
|
||||
if (connected) {
|
||||
document.body.classList.remove('is-disconnected');
|
||||
reconnectButton.disabled = false;
|
||||
reconnectButton.textContent = 'Reconnect';
|
||||
return true;
|
||||
}
|
||||
if (disconnected) {
|
||||
disconnectMessage.textContent = value.charAt(0).toUpperCase() + value.slice(1);
|
||||
document.body.classList.add('is-disconnected');
|
||||
reconnectButton.disabled = false;
|
||||
reconnectButton.textContent = 'Reconnect';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
reconnectButton.addEventListener('click', () => {
|
||||
reconnectButton.disabled = true;
|
||||
reconnectButton.textContent = 'Reconnecting...';
|
||||
disconnectMessage.textContent = 'Reconnecting...';
|
||||
vscode.postMessage({ type: 'reconnect' });
|
||||
});
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
const msg = event.data || {};
|
||||
if (msg.type === 'data') term.write(Uint8Array.from(atob(msg.data), c => c.charCodeAt(0)));
|
||||
if (msg.type === 'status') {
|
||||
if (msg.type === 'state') {
|
||||
// The remote PTY is allocated at a default size before our first resize
|
||||
// can land, so re-send the real geometry once the session is connected.
|
||||
if (setStatus(msg.text)) { resize(true); setTimeout(() => resize(true), 250); }
|
||||
if (setConnectionState(msg.text)) resizeBurst(true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -127,6 +127,7 @@ npm --prefix "$ROOT_DIR/frontend" run build
|
||||
echo "[vscode] Staging shared media assets"
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm.min.js" "$VSCODE_DIR/nexus/media/xterm.min.js"
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-fit.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-fit.min.js"
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-unicode11.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-unicode11.min.js"
|
||||
cp "$ROOT_DIR/frontend/dist/src/css/lib/xterm.css" "$VSCODE_DIR/nexus/media/xterm.css"
|
||||
cp "$ROOT_DIR/frontend/dist/src/js/lib/novnc.min.js" "$VSCODE_DIR/nexus/media/novnc.min.js"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user