refactor: improve code readability and formatting in profiles.js and tabs.js

- Updated formatting for better readability, including consistent use of line breaks and indentation.
- Enhanced the clarity of event listener functions in profiles.js.
- Refactored tab management logic in tabs.js for improved maintainability.
- Ensured consistent use of single quotes and double quotes across the files.
This commit is contained in:
Andy Dixon
2026-04-22 14:57:01 +01:00
parent 8fa9fb5be5
commit 98ca4743e5
6 changed files with 1083 additions and 1002 deletions
+17 -17
View File
@@ -31,20 +31,20 @@ Prefer a one-off session? Use **Cmd+Shift+Q** / **Ctrl+Shift+Q** to quick connec
All shortcuts use **Cmd** on macOS and **Ctrl** on Windows/Linux. All shortcuts use **Cmd** on macOS and **Ctrl** on Windows/Linux.
| macOS Shortcut | Windows/Linux Shortcut | Action | | macOS Shortcut | Windows/Linux Shortcut | Action |
| ------------- | ----------------------- | --------------------------------- | | ----------------- | ---------------------- | --------------------------- |
| Cmd + K | Ctrl + K | Open command palette | | Cmd + K | Ctrl + K | Open command palette |
| Cmd + N | Ctrl + N | New connection | | Cmd + N | Ctrl + N | New connection |
| Cmd + W | Ctrl + W | Close current tab | | Cmd + W | Ctrl + W | Close current tab |
| Cmd + Tab | Ctrl + Tab | Next tab | | Cmd + Tab | Ctrl + Tab | Next tab |
| Cmd + Shift + Tab | Ctrl + Shift + Tab | Previous tab | | Cmd + Shift + Tab | Ctrl + Shift + Tab | Previous tab |
| Cmd + 1–9 | Ctrl + 1–9 | Switch to tab by number | | Cmd + 1–9 | Ctrl + 1–9 | Switch to tab by number |
| Cmd + , | Ctrl + , | Open settings | | Cmd + , | Ctrl + , | Open settings |
| Cmd + F | Ctrl + F | Find in terminal | | Cmd + F | Ctrl + F | Find in terminal |
| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | | Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically |
| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | | Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally |
| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | | Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording |
| Cmd + Shift + B | Ctrl + Shift + B | Toggle broadcast input mode | | Cmd + Shift + B | Ctrl + Shift + B | Toggle broadcast input mode |
| Cmd + Shift + S | Ctrl + Shift + S | Open snippet library | | Cmd + Shift + S | Ctrl + Shift + S | Open snippet library |
| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | | Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect |
| F11 | F11 | Toggle fullscreen | | F11 | F11 | Toggle fullscreen |
+12 -12
View File
@@ -42,18 +42,18 @@ The upload picker lists folders only and marks permissions:
## Common shortcuts ## Common shortcuts
| macOS Shortcut | Windows/Linux Shortcut | Action | | macOS Shortcut | Windows/Linux Shortcut | Action |
| ------------- | ----------------------- | ------------------------ | | --------------- | ---------------------- | ------------------------ |
| Cmd + K | Ctrl + K | Command palette | | Cmd + K | Ctrl + K | Command palette |
| Cmd + N | Ctrl + N | New connection | | Cmd + N | Ctrl + N | New connection |
| Cmd + W | Ctrl + W | Close tab | | Cmd + W | Ctrl + W | Close tab |
| Cmd + F | Ctrl + F | Find in terminal | | Cmd + F | Ctrl + F | Find in terminal |
| Cmd + Shift + S | Ctrl + Shift + S | Snippet library | | Cmd + Shift + S | Ctrl + Shift + S | Snippet library |
| Cmd + Shift + B | Ctrl + Shift + B | Broadcast input | | Cmd + Shift + B | Ctrl + Shift + B | Broadcast input |
| Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect | | Cmd + Shift + Q | Ctrl + Shift + Q | Quick connect |
| Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording | | Cmd + Shift + R | Ctrl + Shift + R | Toggle session recording |
| Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically | | Cmd + Shift + D | Ctrl + Shift + D | Split pane vertically |
| Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally | | Cmd + Shift + E | Ctrl + Shift + E | Split pane horizontally |
## Need more detail? ## Need more detail?
+367 -357
View File
File diff suppressed because it is too large Load Diff
+248 -242
View File
@@ -7,10 +7,10 @@
Terminal wrapper — fills the workspace panel Terminal wrapper — fills the workspace panel
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.terminal-wrapper { .terminal-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
@@ -18,223 +18,227 @@
Shows connection info above the terminal. Shows connection info above the terminal.
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.terminal-header { .terminal-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-1) var(--space-3); padding: var(--space-1) var(--space-3);
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-tertiary); color: var(--text-tertiary);
background: var(--bg-surface); background: var(--bg-surface);
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
min-height: 24px; min-height: 24px;
} }
.terminal-header-label { .terminal-header-label {
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
color: var(--text-secondary); color: var(--text-secondary);
} }
.terminal-header-value { .terminal-header-value {
font-family: var(--font-mono); font-family: var(--font-mono);
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.terminal-header-separator { .terminal-header-separator {
width: 1px; width: 1px;
height: 10px; height: 10px;
background: var(--border-subtle); background: var(--border-subtle);
} }
.terminal-header-spacer { .terminal-header-spacer {
flex: 1; flex: 1;
} }
.terminal-header-actions { .terminal-header-actions {
display: flex; display: flex;
gap: var(--space-1); gap: var(--space-1);
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
xterm.js container xterm.js container
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.terminal-container { .terminal-container {
flex: 1; flex: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Disconnected overlay Disconnected overlay
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.session-panel.is-disconnected > *:not(.terminal-disconnect-overlay) { .session-panel.is-disconnected > *:not(.terminal-disconnect-overlay) {
filter: blur(2px); filter: blur(2px);
pointer-events: none; pointer-events: none;
} }
.terminal-disconnect-overlay { .terminal-disconnect-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(10, 14, 18, 0.68); background: rgba(10, 14, 18, 0.68);
z-index: 6; z-index: 6;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity var(--transition-fast), visibility var(--transition-fast); transition:
opacity var(--transition-fast),
visibility var(--transition-fast);
} }
.terminal-disconnect-overlay.is-visible { .terminal-disconnect-overlay.is-visible {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
.terminal-disconnect-content { .terminal-disconnect-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: var(--space-3); gap: var(--space-3);
text-align: center; text-align: center;
padding: var(--space-4) var(--space-5); padding: var(--space-4) var(--space-5);
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg); box-shadow: var(--shadow-lg);
} }
.terminal-disconnect-title { .terminal-disconnect-title {
font-size: var(--text-lg); font-size: var(--text-lg);
letter-spacing: 0.45em; letter-spacing: 0.45em;
text-indent: 0.45em; text-indent: 0.45em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-primary); color: var(--text-primary);
font-weight: var(--weight-semibold); font-weight: var(--weight-semibold);
} }
.terminal-disconnect-message { .terminal-disconnect-message {
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--text-secondary); color: var(--text-secondary);
max-width: 360px; max-width: 360px;
} }
/* xterm.js overrides for consistent look */ /* xterm.js overrides for consistent look */
.terminal-container .xterm, .terminal-container .xterm,
.session-panel .xterm { .session-panel .xterm {
flex: 1; flex: 1;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
padding: 3px 0 0 3px; padding: 3px 0 0 3px;
} }
.terminal-container .xterm-screen, .terminal-container .xterm-screen,
.session-panel .xterm-screen { .session-panel .xterm-screen {
height: 100%; height: 100%;
} }
.terminal-container .xterm-viewport, .terminal-container .xterm-viewport,
.session-panel .xterm-viewport { .session-panel .xterm-viewport {
/* Allow xterm's own scrollbar to work */ /* Allow xterm's own scrollbar to work */
overflow-y: auto !important; overflow-y: auto !important;
} }
.terminal-container .xterm-viewport::-webkit-scrollbar, .terminal-container .xterm-viewport::-webkit-scrollbar,
.session-panel .xterm-viewport::-webkit-scrollbar { .session-panel .xterm-viewport::-webkit-scrollbar {
width: 6px; width: 6px;
} }
.terminal-container .xterm-viewport::-webkit-scrollbar-track, .terminal-container .xterm-viewport::-webkit-scrollbar-track,
.session-panel .xterm-viewport::-webkit-scrollbar-track { .session-panel .xterm-viewport::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb, .terminal-container .xterm-viewport::-webkit-scrollbar-thumb,
.session-panel .xterm-viewport::-webkit-scrollbar-thumb { .session-panel .xterm-viewport::-webkit-scrollbar-thumb {
background: var(--border-strong); background: var(--border-strong);
border-radius: var(--radius-full); border-radius: var(--radius-full);
} }
.terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover, .terminal-container .xterm-viewport::-webkit-scrollbar-thumb:hover,
.session-panel .xterm-viewport::-webkit-scrollbar-thumb:hover { .session-panel .xterm-viewport::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary); background: var(--text-tertiary);
} }
/* Ensure cursor blink is visible */ /* Ensure cursor blink is visible */
.terminal-container .xterm-cursor-layer, .terminal-container .xterm-cursor-layer,
.session-panel .xterm-cursor-layer { .session-panel .xterm-cursor-layer {
z-index: 1; z-index: 1;
} }
/* Selection styling within terminal */ /* Selection styling within terminal */
.terminal-container .xterm .xterm-selection div, .terminal-container .xterm .xterm-selection div,
.session-panel .xterm .xterm-selection div { .session-panel .xterm .xterm-selection div {
background: var(--accent-strong) !important; background: var(--accent-strong) !important;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Terminal search overlay Terminal search overlay
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.terminal-search { .terminal-search {
position: absolute; position: absolute;
top: var(--space-2); top: var(--space-2);
right: var(--space-4); right: var(--space-4);
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-1); gap: var(--space-1);
padding: var(--space-1); padding: var(--space-1);
background: var(--bg-overlay); background: var(--bg-overlay);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-md); border-radius: var(--radius-md);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
z-index: 10; z-index: 10;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-8px); transform: translateY(-8px);
transition: transition:
opacity var(--transition-fast), opacity var(--transition-fast),
visibility var(--transition-fast), visibility var(--transition-fast),
transform var(--transition-fast); transform var(--transition-fast);
} }
.terminal-search.is-visible { .terminal-search.is-visible {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0);
} }
.terminal-search input { .terminal-search input {
width: 200px; width: 200px;
height: 24px; height: 24px;
padding: 0 var(--space-2); padding: 0 var(--space-2);
font-size: var(--text-sm); font-size: var(--text-sm);
font-family: var(--font-mono); font-family: var(--font-mono);
color: var(--text-primary); color: var(--text-primary);
background: var(--bg-input); background: var(--bg-input);
border: 1px solid var(--border-subtle); border: 1px solid var(--border-subtle);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.terminal-search input:focus { .terminal-search input:focus {
border-color: var(--accent); border-color: var(--accent);
outline: none; outline: none;
} }
.terminal-search-count { .terminal-search-count {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-tertiary); color: var(--text-tertiary);
padding: 0 var(--space-1); padding: 0 var(--space-1);
white-space: nowrap; white-space: nowrap;
min-width: 70px; min-width: 70px;
} }
.terminal-search-toggle { .terminal-search-toggle {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--text-xs); font-size: var(--text-xs);
opacity: 0.5; opacity: 0.5;
transition: opacity var(--transition-fast), color var(--transition-fast); transition:
opacity var(--transition-fast),
color var(--transition-fast);
} }
.terminal-search-toggle.is-active { .terminal-search-toggle.is-active {
opacity: 1; opacity: 1;
color: var(--accent); color: var(--accent);
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
@@ -243,231 +247,233 @@
/* RAW text view — scrollable, monospace output */ /* RAW text view — scrollable, monospace output */
.raw-text-view { .raw-text-view {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: var(--space-3); padding: var(--space-3);
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--text-sm); font-size: var(--text-sm);
line-height: var(--leading-relaxed); line-height: var(--leading-relaxed);
color: var(--text-primary); color: var(--text-primary);
background: var(--bg-base); background: var(--bg-base);
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
} }
/* RAW hex view */ /* RAW hex view */
.raw-hex-view { .raw-hex-view {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: var(--space-3); padding: var(--space-3);
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--text-xs); font-size: var(--text-xs);
line-height: var(--leading-relaxed); line-height: var(--leading-relaxed);
color: var(--text-primary); color: var(--text-primary);
background: var(--bg-base); background: var(--bg-base);
} }
.raw-hex-row { .raw-hex-row {
display: flex; display: flex;
gap: var(--space-4); gap: var(--space-4);
} }
.raw-hex-offset { .raw-hex-offset {
color: var(--text-tertiary); color: var(--text-tertiary);
min-width: 60px; min-width: 60px;
} }
.raw-hex-bytes { .raw-hex-bytes {
min-width: 350px; min-width: 350px;
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
.raw-hex-bytes .byte-highlight { .raw-hex-bytes .byte-highlight {
color: var(--accent); color: var(--accent);
} }
.raw-hex-ascii { .raw-hex-ascii {
color: var(--text-secondary); color: var(--text-secondary);
} }
.raw-hex-ascii .non-printable { .raw-hex-ascii .non-printable {
color: var(--text-disabled); color: var(--text-disabled);
} }
/* RAW split view (text on left, hex on right) */ /* RAW split view (text on left, hex on right) */
.raw-split-view { .raw-split-view {
flex: 1; flex: 1;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
} }
.raw-split-view .raw-text-view, .raw-split-view .raw-text-view,
.raw-split-view .raw-hex-view { .raw-split-view .raw-hex-view {
flex: 1; flex: 1;
} }
.raw-split-divider { .raw-split-divider {
width: 1px; width: 1px;
background: var(--border-subtle); background: var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
RAW mode view toggle buttons RAW mode view toggle buttons
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.raw-view-toggle { .raw-view-toggle {
display: flex; display: flex;
gap: 1px; gap: 1px;
background: var(--border-subtle); background: var(--border-subtle);
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
} }
.raw-view-toggle-btn { .raw-view-toggle-btn {
padding: 0 var(--space-2); padding: 0 var(--space-2);
height: 22px; height: 22px;
font-size: var(--text-xs); font-size: var(--text-xs);
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
color: var(--text-tertiary); color: var(--text-tertiary);
background: var(--bg-surface); background: var(--bg-surface);
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background var(--transition-fast), color var(--transition-fast); transition:
background var(--transition-fast),
color var(--transition-fast);
} }
.raw-view-toggle-btn:hover { .raw-view-toggle-btn:hover {
color: var(--text-secondary); color: var(--text-secondary);
} }
.raw-view-toggle-btn.active { .raw-view-toggle-btn.active {
background: var(--bg-active); background: var(--bg-active);
color: var(--text-primary); color: var(--text-primary);
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Send bar (RAW TCP mode — manual data entry) Send bar (RAW TCP mode — manual data entry)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.send-bar { .send-bar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-2); gap: var(--space-2);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
background: var(--bg-surface); background: var(--bg-surface);
border-top: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
.send-bar-input { .send-bar-input {
flex: 1; flex: 1;
height: 28px; height: 28px;
padding: 0 var(--space-3); padding: 0 var(--space-3);
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--text-primary); color: var(--text-primary);
background: var(--bg-input); background: var(--bg-input);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-md); border-radius: var(--radius-md);
transition: border-color var(--transition-fast); transition: border-color var(--transition-fast);
} }
.send-bar-input:focus { .send-bar-input:focus {
border-color: var(--accent); border-color: var(--accent);
outline: none; outline: none;
} }
.send-bar-input::placeholder { .send-bar-input::placeholder {
color: var(--text-disabled); color: var(--text-disabled);
} }
/* Line ending selector */ /* Line ending selector */
.send-bar-eol { .send-bar-eol {
height: 28px; height: 28px;
padding: 0 var(--space-2); padding: 0 var(--space-2);
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-secondary); color: var(--text-secondary);
background: var(--bg-input); background: var(--bg-input);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
border-radius: var(--radius-md); border-radius: var(--radius-md);
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
min-width: 60px; min-width: 60px;
} }
.send-bar-eol:focus { .send-bar-eol:focus {
border-color: var(--accent); border-color: var(--accent);
outline: none; outline: none;
} }
/* Encoding selector */ /* Encoding selector */
.send-bar-encoding { .send-bar-encoding {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-tertiary); color: var(--text-tertiary);
padding: 0 var(--space-1); padding: 0 var(--space-1);
} }
/* ----------------------------------------------------------------------- /* -----------------------------------------------------------------------
Session overlay (connecting / error states) Session overlay (connecting / error states)
----------------------------------------------------------------------- */ ----------------------------------------------------------------------- */
.session-overlay { .session-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(28, 28, 28, 0.85); background: rgba(28, 28, 28, 0.85);
z-index: 5; z-index: 5;
} }
.session-overlay-connecting { .session-overlay-connecting {
text-align: center; text-align: center;
} }
.session-overlay-connecting .spinner { .session-overlay-connecting .spinner {
width: 24px; width: 24px;
height: 24px; height: 24px;
margin: 0 auto var(--space-4); margin: 0 auto var(--space-4);
} }
.session-overlay-connecting p { .session-overlay-connecting p {
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--text-secondary); color: var(--text-secondary);
} }
.session-overlay-error { .session-overlay-error {
text-align: center; text-align: center;
max-width: 360px; max-width: 360px;
} }
.session-overlay-error h3 { .session-overlay-error h3 {
font-size: var(--text-md); font-size: var(--text-md);
font-weight: var(--weight-semibold); font-weight: var(--weight-semibold);
color: var(--error); color: var(--error);
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
} }
.session-overlay-error p { .session-overlay-error p {
font-size: var(--text-sm); font-size: var(--text-sm);
color: var(--text-tertiary); color: var(--text-tertiary);
line-height: var(--leading-relaxed); line-height: var(--leading-relaxed);
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
} }
.session-overlay-error .session-error-detail { .session-overlay-error .session-error-detail {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-disabled); color: var(--text-disabled);
background: var(--bg-input); background: var(--bg-input);
padding: var(--space-2) var(--space-3); padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
text-align: left; text-align: left;
max-height: 120px; max-height: 120px;
overflow-y: auto; overflow-y: auto;
word-break: break-all; word-break: break-all;
} }
.session-overlay-actions { .session-overlay-actions {
display: flex; display: flex;
gap: var(--space-2); gap: var(--space-2);
} }
+21 -18
View File
@@ -1659,7 +1659,9 @@
<button class="btn btn-icon btn-danger jump-host-remove" data-index="${i}" title="Remove">&times;</button> <button class="btn btn-icon btn-danger jump-host-remove" data-index="${i}" title="Remove">&times;</button>
</div> </div>
`; `;
const keyInput = row.querySelector(`input[name="jumpHost-${i}-privateKeyPath"]`); const keyInput = row.querySelector(
`input[name="jumpHost-${i}-privateKeyPath"]`,
);
const browseBtn = row.querySelector(".jump-host-browse"); const browseBtn = row.querySelector(".jump-host-browse");
const error = createElement("span", "form-error"); const error = createElement("span", "form-error");
error.style.display = "none"; error.style.display = "none";
@@ -1712,7 +1714,9 @@
_attachKeyPathValidation(input, errorEl, successEl) { _attachKeyPathValidation(input, errorEl, successEl) {
if (!input || !errorEl) return; if (!input || !errorEl) return;
input.addEventListener("blur", () => this._validateKeyPath(input, errorEl, successEl)); input.addEventListener("blur", () =>
this._validateKeyPath(input, errorEl, successEl),
);
input.addEventListener("input", () => { input.addEventListener("input", () => {
errorEl.style.display = "none"; errorEl.style.display = "none";
errorEl.textContent = ""; errorEl.textContent = "";
@@ -1757,24 +1761,23 @@
} }
return; return;
} }
return window.go.api.Service.FileIsPrivateKey(value) return window.go.api.Service.FileIsPrivateKey(value).then((isKey) => {
.then((isKey) => { if (!isKey) {
if (!isKey) { errorEl.textContent = "File is not a recognised private key.";
errorEl.textContent = "File is not a recognised private key."; errorEl.style.display = "block";
errorEl.style.display = "block";
if (successEl) {
successEl.style.display = "none";
successEl.textContent = "";
}
return;
}
errorEl.style.display = "none";
errorEl.textContent = "";
if (successEl) { if (successEl) {
successEl.textContent = "Key verified."; successEl.style.display = "none";
successEl.style.display = "block"; successEl.textContent = "";
} }
}); return;
}
errorEl.style.display = "none";
errorEl.textContent = "";
if (successEl) {
successEl.textContent = "Key verified.";
successEl.style.display = "block";
}
});
}) })
.catch(() => { .catch(() => {
errorEl.textContent = "Could not verify this path."; errorEl.textContent = "Could not verify this path.";
+418 -356
View File
@@ -1,32 +1,38 @@
(function() { (function () {
/** /**
* Nexus Terminal — tab management. * Nexus Terminal — tab management.
* Handles the tab bar: adding, removing, reordering, renaming, * Handles the tab bar: adding, removing, reordering, renaming,
* pinning tabs, and context menus. * pinning tabs, and context menus.
*/ */
const { createElement, escapeHtml, protocolColour, stateLabel, dismissContextMenus } = window.NexusTerminal; const {
createElement,
escapeHtml,
protocolColour,
stateLabel,
dismissContextMenus,
} = window.NexusTerminal;
class TabManager { class TabManager {
/** /**
* @param {App} app * @param {App} app
*/ */
constructor(app) { constructor(app) {
this.app = app; this.app = app;
/** @type {HTMLElement} The inner tab list where tab elements are inserted. */ /** @type {HTMLElement} The inner tab list where tab elements are inserted. */
this.tabList = document.getElementById('tab-list'); this.tabList = document.getElementById("tab-list");
/** @type {HTMLElement} The outer tab bar wrapper (used for drag-and-drop). */ /** @type {HTMLElement} The outer tab bar wrapper (used for drag-and-drop). */
this.tabBar = document.getElementById('tab-bar'); this.tabBar = document.getElementById("tab-bar");
/** @type {HTMLElement} The container for terminal/viewer panels. */ /** @type {HTMLElement} The container for terminal/viewer panels. */
this.panelContainer = document.getElementById('panel-container'); this.panelContainer = document.getElementById("panel-container");
/** @type {string[]} Ordered list of session IDs. */ /** @type {string[]} Ordered list of session IDs. */
this.order = []; this.order = [];
this._setupDragAndDrop(); this._setupDragAndDrop();
} }
// ── Public API ─────────────────────────────────────────────────── // ── Public API ───────────────────────────────────────────────────
@@ -36,21 +42,21 @@ class TabManager {
* @param {Object} sessionInfo * @param {Object} sessionInfo
*/ */
addTab(sessionInfo) { addTab(sessionInfo) {
this.order.push(sessionInfo.id); this.order.push(sessionInfo.id);
this._renderTab(sessionInfo); this._renderTab(sessionInfo);
// Create panel container for this session's terminal/viewer // Create panel container for this session's terminal/viewer
const panel = createElement('div', 'session-panel', { const panel = createElement("div", "session-panel", {
id: `panel-${sessionInfo.id}`, id: `panel-${sessionInfo.id}`,
dataset: { sessionId: sessionInfo.id }, dataset: { sessionId: sessionInfo.id },
}); });
panel.style.display = 'none'; panel.style.display = "none";
this.panelContainer.appendChild(panel); this.panelContainer.appendChild(panel);
// Hide the empty-state BEFORE activating the panel so the // Hide the empty-state BEFORE activating the panel so the
// panel-container gets the full workspace height immediately. // panel-container gets the full workspace height immediately.
this._updateEmptyState(); this._updateEmptyState();
this.setActive(sessionInfo.id); this.setActive(sessionInfo.id);
} }
/** /**
@@ -58,30 +64,33 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
removeTab(sessionId) { removeTab(sessionId) {
const idx = this.order.indexOf(sessionId); const idx = this.order.indexOf(sessionId);
if (idx !== -1) this.order.splice(idx, 1); if (idx !== -1) this.order.splice(idx, 1);
// Remove tab element // Remove tab element
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); const tabEl = this.tabList.querySelector(
if (tabEl) tabEl.remove(); `[data-session-id="${sessionId}"]`,
);
if (tabEl) tabEl.remove();
// Remove panel // Remove panel
const panel = document.getElementById(`panel-${sessionId}`); const panel = document.getElementById(`panel-${sessionId}`);
if (panel) panel.remove(); if (panel) panel.remove();
// If this was the active tab, activate the nearest neighbour // If this was the active tab, activate the nearest neighbour
if (this.app.state.activeSessionId === sessionId) { if (this.app.state.activeSessionId === sessionId) {
const next = this.order.length > 0 const next =
? this.order[Math.min(idx, this.order.length - 1)] this.order.length > 0
: null; ? this.order[Math.min(idx, this.order.length - 1)]
if (next) { : null;
this.setActive(next); if (next) {
} else { this.setActive(next);
this.app.state.setActiveSession(null); } else {
} this.app.state.setActiveSession(null);
} }
}
this._updateEmptyState(); this._updateEmptyState();
} }
/** /**
@@ -89,35 +98,35 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
setActive(sessionId) { setActive(sessionId) {
this.app.state.setActiveSession(sessionId); this.app.state.setActiveSession(sessionId);
// Highlight active tab // Highlight active tab
this.tabList.querySelectorAll('.tab').forEach((t) => { this.tabList.querySelectorAll(".tab").forEach((t) => {
t.classList.toggle('tab-active', t.dataset.sessionId === sessionId); t.classList.toggle("tab-active", t.dataset.sessionId === sessionId);
}); });
// Show corresponding panel, hide others // Show corresponding panel, hide others
this.panelContainer.querySelectorAll('.session-panel').forEach((p) => { this.panelContainer.querySelectorAll(".session-panel").forEach((p) => {
p.style.display = p.dataset.sessionId === sessionId ? 'flex' : 'none'; p.style.display = p.dataset.sessionId === sessionId ? "flex" : "none";
}); });
// Notify sidebar // Notify sidebar
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
if (session) { if (session) {
this.app.sidebar.highlightActive(session.profileId); this.app.sidebar.highlightActive(session.profileId);
} }
// Fit the terminal for the newly active session // Fit the terminal for the newly active session
this.app.terminals.fitActive(); this.app.terminals.fitActive();
} }
/** /**
* Closes the currently active tab. * Closes the currently active tab.
*/ */
closeActive() { closeActive() {
if (this.app.state.activeSessionId) { if (this.app.state.activeSessionId) {
this.closeTab(this.app.state.activeSessionId); this.closeTab(this.app.state.activeSessionId);
} }
} }
/** /**
@@ -125,7 +134,7 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
reconnectSession(sessionId) { reconnectSession(sessionId) {
return this._reconnect(sessionId); return this._reconnect(sessionId);
} }
/** /**
@@ -133,25 +142,29 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
async closeTab(sessionId) { async closeTab(sessionId) {
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
// Ask for confirmation if connected and the preference is set // Ask for confirmation if connected and the preference is set
if (session && session.state === 'connected' && this.app.state.preferences.confirmOnDisconnect) { if (
const ok = await this._confirmClose(sessionId); session &&
if (!ok) return; session.state === "connected" &&
} this.app.state.preferences.confirmOnDisconnect
) {
const ok = await this._confirmClose(sessionId);
if (!ok) return;
}
try { try {
await window.go.api.Service.DisconnectSession(sessionId); await window.go.api.Service.DisconnectSession(sessionId);
} catch (_) { } catch (_) {
// Session may already be closed — carry on // Session may already be closed — carry on
} }
this.app.terminals.destroyTerminal(sessionId); this.app.terminals.destroyTerminal(sessionId);
this.app.rawViewer.destroyViewer(sessionId); this.app.rawViewer.destroyViewer(sessionId);
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId); window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
this.app.state.removeSession(sessionId); this.app.state.removeSession(sessionId);
this.removeTab(sessionId); this.removeTab(sessionId);
} }
/** /**
@@ -161,12 +174,12 @@ class TabManager {
* @private * @private
*/ */
_confirmClose(sessionId) { _confirmClose(sessionId) {
const createElement = window.NexusTerminal.createElement; const createElement = window.NexusTerminal.createElement;
const label = this._tabLabel(sessionId); const label = this._tabLabel(sessionId);
return new Promise((resolve) => { return new Promise((resolve) => {
const backdrop = createElement('div', 'modal-backdrop'); const backdrop = createElement("div", "modal-backdrop");
const modal = createElement('div', 'modal modal-sm'); const modal = createElement("div", "modal modal-sm");
modal.innerHTML = ` modal.innerHTML = `
<div class="modal-header"> <div class="modal-header">
<h3 class="modal-title">Close Session</h3> <h3 class="modal-title">Close Session</h3>
<button class="btn btn-icon btn-xs modal-close">&times;</button> <button class="btn btn-icon btn-xs modal-close">&times;</button>
@@ -181,24 +194,32 @@ class TabManager {
<button class="btn btn-sm btn-danger confirm-ok">Disconnect</button> <button class="btn btn-sm btn-danger confirm-ok">Disconnect</button>
</div> </div>
`; `;
backdrop.appendChild(modal); backdrop.appendChild(modal);
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
requestAnimationFrame(() => { requestAnimationFrame(() => {
backdrop.classList.add('is-visible'); backdrop.classList.add("is-visible");
modal.classList.add('is-visible'); modal.classList.add("is-visible");
});
const close = (val) => {
backdrop.classList.remove('is-visible');
modal.classList.remove('is-visible');
setTimeout(() => backdrop.remove(), 200);
resolve(val);
};
modal.querySelector('.modal-close').addEventListener('click', () => close(false));
modal.querySelector('.confirm-cancel').addEventListener('click', () => close(false));
modal.querySelector('.confirm-ok').addEventListener('click', () => close(true));
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close(false); });
}); });
const close = (val) => {
backdrop.classList.remove("is-visible");
modal.classList.remove("is-visible");
setTimeout(() => backdrop.remove(), 200);
resolve(val);
};
modal
.querySelector(".modal-close")
.addEventListener("click", () => close(false));
modal
.querySelector(".confirm-cancel")
.addEventListener("click", () => close(false));
modal
.querySelector(".confirm-ok")
.addEventListener("click", () => close(true));
backdrop.addEventListener("click", (e) => {
if (e.target === backdrop) close(false);
});
});
} }
/** /**
@@ -207,11 +228,13 @@ class TabManager {
* @param {string} name * @param {string} name
*/ */
renameTab(sessionId, name) { renameTab(sessionId, name) {
this.app.state.updateSession(sessionId, { tabName: name }); this.app.state.updateSession(sessionId, { tabName: name });
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); const tabEl = this.tabList.querySelector(
if (tabEl) { `[data-session-id="${sessionId}"]`,
tabEl.querySelector('.tab-name').textContent = name; );
} if (tabEl) {
tabEl.querySelector(".tab-name").textContent = name;
}
} }
/** /**
@@ -219,9 +242,9 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
pinTab(sessionId) { pinTab(sessionId) {
this.app.state.updateSession(sessionId, { pinned: true }); this.app.state.updateSession(sessionId, { pinned: true });
this._reorderDOM(); this._reorderDOM();
this._updateTabStyles(sessionId); this._updateTabStyles(sessionId);
} }
/** /**
@@ -229,9 +252,9 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
unpinTab(sessionId) { unpinTab(sessionId) {
this.app.state.updateSession(sessionId, { pinned: false }); this.app.state.updateSession(sessionId, { pinned: false });
this._reorderDOM(); this._reorderDOM();
this._updateTabStyles(sessionId); this._updateTabStyles(sessionId);
} }
/** /**
@@ -239,11 +262,14 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
moveLeft(sessionId) { moveLeft(sessionId) {
const idx = this.order.indexOf(sessionId); const idx = this.order.indexOf(sessionId);
if (idx > 0) { if (idx > 0) {
[this.order[idx - 1], this.order[idx]] = [this.order[idx], this.order[idx - 1]]; [this.order[idx - 1], this.order[idx]] = [
this._reorderDOM(); this.order[idx],
} this.order[idx - 1],
];
this._reorderDOM();
}
} }
/** /**
@@ -251,11 +277,14 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
moveRight(sessionId) { moveRight(sessionId) {
const idx = this.order.indexOf(sessionId); const idx = this.order.indexOf(sessionId);
if (idx >= 0 && idx < this.order.length - 1) { if (idx >= 0 && idx < this.order.length - 1) {
[this.order[idx], this.order[idx + 1]] = [this.order[idx + 1], this.order[idx]]; [this.order[idx], this.order[idx + 1]] = [
this._reorderDOM(); this.order[idx + 1],
} this.order[idx],
];
this._reorderDOM();
}
} }
/** /**
@@ -263,10 +292,10 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
async duplicateTab(sessionId) { async duplicateTab(sessionId) {
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
if (session && session.profileId) { if (session && session.profileId) {
await this.app.connectProfile(session.profileId); await this.app.connectProfile(session.profileId);
} }
} }
/** /**
@@ -276,20 +305,22 @@ class TabManager {
* @param {string} [error] * @param {string} [error]
*/ */
updateSessionState(sessionId, state, error) { updateSessionState(sessionId, state, error) {
this.app.state.updateSession(sessionId, { state, error: error || '' }); this.app.state.updateSession(sessionId, { state, error: error || "" });
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); const tabEl = this.tabList.querySelector(
if (!tabEl) return; `[data-session-id="${sessionId}"]`,
);
if (!tabEl) return;
const dot = tabEl.querySelector('.tab-status'); const dot = tabEl.querySelector(".tab-status");
if (dot) { if (dot) {
dot.className = `tab-status status-${state}`; dot.className = `tab-status status-${state}`;
dot.title = stateLabel(state); dot.title = stateLabel(state);
} }
if (state === 'error' && error) { if (state === "error" && error) {
this.app.notifications.error(`${this._tabLabel(sessionId)}: ${error}`); this.app.notifications.error(`${this._tabLabel(sessionId)}: ${error}`);
} }
} }
/** /**
@@ -297,7 +328,7 @@ class TabManager {
* @param {string} sessionId * @param {string} sessionId
*/ */
handleSessionClosed(sessionId) { handleSessionClosed(sessionId) {
this.updateSessionState(sessionId, 'disconnected'); this.updateSessionState(sessionId, "disconnected");
} }
/** /**
@@ -307,58 +338,73 @@ class TabManager {
* @param {number} y * @param {number} y
*/ */
showContextMenu(sessionId, x, y) { showContextMenu(sessionId, x, y) {
dismissContextMenus(); dismissContextMenus();
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
if (!session) return; if (!session) return;
const menu = createElement('div', 'context-menu'); const menu = createElement("div", "context-menu");
menu.style.left = `${x}px`; menu.style.left = `${x}px`;
menu.style.top = `${y}px`; menu.style.top = `${y}px`;
const isPinned = session.pinned; const isPinned = session.pinned;
const items = [ const items = [
{ label: 'Rename…', action: () => this._promptRename(sessionId) }, { label: "Rename…", action: () => this._promptRename(sessionId) },
{ label: 'Duplicate', action: () => this.duplicateTab(sessionId) }, { label: "Duplicate", action: () => this.duplicateTab(sessionId) },
{ divider: true }, { divider: true },
{ label: isPinned ? 'Unpin' : 'Pin', action: () => isPinned ? this.unpinTab(sessionId) : this.pinTab(sessionId) }, {
{ label: 'Move Left', action: () => this.moveLeft(sessionId) }, label: isPinned ? "Unpin" : "Pin",
{ label: 'Move Right', action: () => this.moveRight(sessionId) }, action: () =>
{ divider: true }, isPinned ? this.unpinTab(sessionId) : this.pinTab(sessionId),
{ },
label: 'Reconnect', { label: "Move Left", action: () => this.moveLeft(sessionId) },
action: () => this._reconnect(sessionId), { label: "Move Right", action: () => this.moveRight(sessionId) },
disabled: session.state === 'connected' || session.state === 'connecting', { divider: true },
}, {
{ divider: true }, label: "Reconnect",
{ label: 'Close', className: 'context-menu-danger', action: () => this.closeTab(sessionId) }, action: () => this._reconnect(sessionId),
]; disabled:
session.state === "connected" || session.state === "connecting",
},
{ divider: true },
{
label: "Close",
className: "context-menu-danger",
action: () => this.closeTab(sessionId),
},
];
for (const entry of items) { for (const entry of items) {
if (entry.divider) { if (entry.divider) {
menu.appendChild(createElement('div', 'context-menu-divider')); menu.appendChild(createElement("div", "context-menu-divider"));
continue; continue;
}
const btn = createElement('button', `context-menu-item ${entry.className || ''}`, {
textContent: entry.label,
});
if (entry.disabled) {
btn.disabled = true;
btn.classList.add('context-menu-disabled');
}
btn.addEventListener('click', () => {
dismissContextMenus();
entry.action();
});
menu.appendChild(btn);
} }
const btn = createElement(
"button",
`context-menu-item ${entry.className || ""}`,
{
textContent: entry.label,
},
);
if (entry.disabled) {
btn.disabled = true;
btn.classList.add("context-menu-disabled");
}
btn.addEventListener("click", () => {
dismissContextMenus();
entry.action();
});
menu.appendChild(btn);
}
document.body.appendChild(menu); document.body.appendChild(menu);
// Keep within viewport // Keep within viewport
const rect = menu.getBoundingClientRect(); const rect = menu.getBoundingClientRect();
if (rect.right > window.innerWidth) menu.style.left = `${window.innerWidth - rect.width - 8}px`; if (rect.right > window.innerWidth)
if (rect.bottom > window.innerHeight) menu.style.top = `${window.innerHeight - rect.height - 8}px`; menu.style.left = `${window.innerWidth - rect.width - 8}px`;
if (rect.bottom > window.innerHeight)
menu.style.top = `${window.innerHeight - rect.height - 8}px`;
} }
/** /**
@@ -366,7 +412,7 @@ class TabManager {
* @returns {string[]} * @returns {string[]}
*/ */
getTabOrder() { getTabOrder() {
return [...this.order]; return [...this.order];
} }
/** /**
@@ -374,9 +420,9 @@ class TabManager {
* @returns {string|null} * @returns {string|null}
*/ */
getNextTab() { getNextTab() {
if (this.order.length === 0) return null; if (this.order.length === 0) return null;
const idx = this.order.indexOf(this.app.state.activeSessionId); const idx = this.order.indexOf(this.app.state.activeSessionId);
return this.order[(idx + 1) % this.order.length]; return this.order[(idx + 1) % this.order.length];
} }
/** /**
@@ -384,9 +430,9 @@ class TabManager {
* @returns {string|null} * @returns {string|null}
*/ */
getPrevTab() { getPrevTab() {
if (this.order.length === 0) return null; if (this.order.length === 0) return null;
const idx = this.order.indexOf(this.app.state.activeSessionId); const idx = this.order.indexOf(this.app.state.activeSessionId);
return this.order[(idx - 1 + this.order.length) % this.order.length]; return this.order[(idx - 1 + this.order.length) % this.order.length];
} }
// ── Rendering ──────────────────────────────────────────────────── // ── Rendering ────────────────────────────────────────────────────
@@ -397,70 +443,81 @@ class TabManager {
* @private * @private
*/ */
_renderTab(sessionInfo) { _renderTab(sessionInfo) {
const tab = createElement('div', 'tab', { const tab = createElement("div", "tab", {
dataset: { sessionId: sessionInfo.id }, dataset: { sessionId: sessionInfo.id },
draggable: 'true', draggable: "true",
});
// Protocol badge
const protoLabel =
sessionInfo.protocol === "github"
? "GitHub"
: sessionInfo.protocol.toUpperCase();
const badge = createElement(
"span",
`tab-badge badge-${sessionInfo.protocol}`,
{
textContent: protoLabel,
},
);
tab.appendChild(badge);
// Name
const name = createElement("span", "tab-name", {
textContent: sessionInfo.tabName || sessionInfo.protocol.toUpperCase(),
});
tab.appendChild(name);
// Status dot
const status = createElement(
"span",
`tab-status status-${sessionInfo.state || "connecting"}`,
{
title: stateLabel(sessionInfo.state),
},
);
tab.appendChild(status);
// Pin indicator
if (sessionInfo.pinned) {
tab.classList.add("tab-pinned");
}
// SFTP toggle button (SSH sessions only)
if (sessionInfo.protocol === "ssh") {
const sftpBtn = createElement("button", "tab-sftp", {
textContent: "📁",
title: "Toggle SFTP file browser",
"aria-label": "Toggle SFTP",
}); });
sftpBtn.addEventListener("click", (e) => {
// Protocol badge e.stopPropagation();
const protoLabel = sessionInfo.protocol === 'github' ? 'GitHub' : sessionInfo.protocol.toUpperCase(); this.app.sftp.toggle(sessionInfo.id);
const badge = createElement('span', `tab-badge badge-${sessionInfo.protocol}`, {
textContent: protoLabel,
}); });
tab.appendChild(badge); tab.appendChild(sftpBtn);
}
// Name // Close button
const name = createElement('span', 'tab-name', { const closeBtn = createElement("button", "tab-close", {
textContent: sessionInfo.tabName || sessionInfo.protocol.toUpperCase(), textContent: "×",
}); "aria-label": "Close tab",
tab.appendChild(name); });
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.closeTab(sessionInfo.id);
});
tab.appendChild(closeBtn);
// Status dot // Click → activate
const status = createElement('span', `tab-status status-${sessionInfo.state || 'connecting'}`, { tab.addEventListener("click", () => this.setActive(sessionInfo.id));
title: stateLabel(sessionInfo.state),
});
tab.appendChild(status);
// Pin indicator // Right-click → context menu
if (sessionInfo.pinned) { tab.addEventListener("contextmenu", (e) => {
tab.classList.add('tab-pinned'); e.preventDefault();
} this.showContextMenu(sessionInfo.id, e.clientX, e.clientY);
});
// SFTP toggle button (SSH sessions only) this.tabList.appendChild(tab);
if (sessionInfo.protocol === 'ssh') {
const sftpBtn = createElement('button', 'tab-sftp', {
textContent: '📁',
title: 'Toggle SFTP file browser',
'aria-label': 'Toggle SFTP',
});
sftpBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.app.sftp.toggle(sessionInfo.id);
});
tab.appendChild(sftpBtn);
}
// Close button
const closeBtn = createElement('button', 'tab-close', {
textContent: '×',
'aria-label': 'Close tab',
});
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.closeTab(sessionInfo.id);
});
tab.appendChild(closeBtn);
// Click → activate
tab.addEventListener('click', () => this.setActive(sessionInfo.id));
// Right-click → context menu
tab.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showContextMenu(sessionInfo.id, e.clientX, e.clientY);
});
this.tabList.appendChild(tab);
} }
// ── Internal helpers ───────────────────────────────────────────── // ── Internal helpers ─────────────────────────────────────────────
@@ -471,20 +528,20 @@ class TabManager {
* @private * @private
*/ */
_reorderDOM() { _reorderDOM() {
// Sort: pinned first, then by order index // Sort: pinned first, then by order index
const sorted = [...this.order].sort((a, b) => { const sorted = [...this.order].sort((a, b) => {
const sa = this.app.state.getSession(a); const sa = this.app.state.getSession(a);
const sb = this.app.state.getSession(b); const sb = this.app.state.getSession(b);
const pinnedA = sa?.pinned ? 0 : 1; const pinnedA = sa?.pinned ? 0 : 1;
const pinnedB = sb?.pinned ? 0 : 1; const pinnedB = sb?.pinned ? 0 : 1;
if (pinnedA !== pinnedB) return pinnedA - pinnedB; if (pinnedA !== pinnedB) return pinnedA - pinnedB;
return this.order.indexOf(a) - this.order.indexOf(b); return this.order.indexOf(a) - this.order.indexOf(b);
}); });
for (const id of sorted) { for (const id of sorted) {
const el = this.tabList.querySelector(`[data-session-id="${id}"]`); const el = this.tabList.querySelector(`[data-session-id="${id}"]`);
if (el) this.tabList.appendChild(el); if (el) this.tabList.appendChild(el);
} }
} }
/** /**
@@ -493,11 +550,13 @@ class TabManager {
* @private * @private
*/ */
_updateTabStyles(sessionId) { _updateTabStyles(sessionId) {
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
const tabEl = this.tabList.querySelector(`[data-session-id="${sessionId}"]`); const tabEl = this.tabList.querySelector(
if (tabEl && session) { `[data-session-id="${sessionId}"]`,
tabEl.classList.toggle('tab-pinned', !!session.pinned); );
} if (tabEl && session) {
tabEl.classList.toggle("tab-pinned", !!session.pinned);
}
} }
/** /**
@@ -505,10 +564,10 @@ class TabManager {
* @private * @private
*/ */
_updateEmptyState() { _updateEmptyState() {
const empty = document.getElementById('empty-state'); const empty = document.getElementById("empty-state");
if (empty) { if (empty) {
empty.style.display = this.order.length === 0 ? 'flex' : 'none'; empty.style.display = this.order.length === 0 ? "flex" : "none";
} }
} }
/** /**
@@ -517,14 +576,14 @@ class TabManager {
* @private * @private
*/ */
_promptRename(sessionId) { _promptRename(sessionId) {
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
const current = session?.tabName || ''; const current = session?.tabName || "";
// Use the app's custom prompt modal (window.prompt is unavailable in WKWebView) // Use the app's custom prompt modal (window.prompt is unavailable in WKWebView)
this.app._inputPrompt('Rename Tab', 'Tab name', current).then((name) => { this.app._inputPrompt("Rename Tab", "Tab name", current).then((name) => {
if (name !== null && name.trim()) { if (name !== null && name.trim()) {
this.renameTab(sessionId, name.trim()); this.renameTab(sessionId, name.trim());
} }
}); });
} }
/** /**
@@ -533,17 +592,17 @@ class TabManager {
* @private * @private
*/ */
async _reconnect(sessionId) { async _reconnect(sessionId) {
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
if (!session || !session.profileId) return; if (!session || !session.profileId) return;
// Close the current tab, then open a fresh connection // Close the current tab, then open a fresh connection
this.app.terminals.destroyTerminal(sessionId); this.app.terminals.destroyTerminal(sessionId);
this.app.rawViewer.destroyViewer(sessionId); this.app.rawViewer.destroyViewer(sessionId);
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId); window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
this.app.state.removeSession(sessionId); this.app.state.removeSession(sessionId);
this.removeTab(sessionId); this.removeTab(sessionId);
await this.app.connectProfile(session.profileId); await this.app.connectProfile(session.profileId);
} }
/** /**
@@ -553,8 +612,8 @@ class TabManager {
* @private * @private
*/ */
_tabLabel(sessionId) { _tabLabel(sessionId) {
const session = this.app.state.getSession(sessionId); const session = this.app.state.getSession(sessionId);
return session?.tabName || 'Session'; return session?.tabName || "Session";
} }
// ── Drag and drop ──────────────────────────────────────────────── // ── Drag and drop ────────────────────────────────────────────────
@@ -564,68 +623,71 @@ class TabManager {
* @private * @private
*/ */
_setupDragAndDrop() { _setupDragAndDrop() {
if (!this.tabList) return; if (!this.tabList) return;
let draggedId = null; let draggedId = null;
this.tabList.addEventListener('dragstart', (e) => { this.tabList.addEventListener("dragstart", (e) => {
const tab = e.target.closest('.tab'); const tab = e.target.closest(".tab");
if (!tab) return; if (!tab) return;
draggedId = tab.dataset.sessionId; draggedId = tab.dataset.sessionId;
tab.classList.add('tab-dragging'); tab.classList.add("tab-dragging");
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = "move";
}); });
this.tabList.addEventListener('dragover', (e) => { this.tabList.addEventListener("dragover", (e) => {
e.preventDefault(); e.preventDefault();
e.dataTransfer.dropEffect = 'move'; e.dataTransfer.dropEffect = "move";
const target = e.target.closest('.tab'); const target = e.target.closest(".tab");
if (!target || target.dataset.sessionId === draggedId) return; if (!target || target.dataset.sessionId === draggedId) return;
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
const midX = rect.left + rect.width / 2; const midX = rect.left + rect.width / 2;
target.classList.toggle('tab-drop-before', e.clientX < midX); target.classList.toggle("tab-drop-before", e.clientX < midX);
target.classList.toggle('tab-drop-after', e.clientX >= midX); target.classList.toggle("tab-drop-after", e.clientX >= midX);
}); });
this.tabList.addEventListener('dragleave', (e) => { this.tabList.addEventListener("dragleave", (e) => {
const target = e.target.closest('.tab'); const target = e.target.closest(".tab");
if (target) { if (target) {
target.classList.remove('tab-drop-before', 'tab-drop-after'); target.classList.remove("tab-drop-before", "tab-drop-after");
} }
}); });
this.tabList.addEventListener('drop', (e) => { this.tabList.addEventListener("drop", (e) => {
e.preventDefault(); e.preventDefault();
const target = e.target.closest('.tab'); const target = e.target.closest(".tab");
if (!target || !draggedId) return; if (!target || !draggedId) return;
target.classList.remove('tab-drop-before', 'tab-drop-after'); target.classList.remove("tab-drop-before", "tab-drop-after");
const targetId = target.dataset.sessionId; const targetId = target.dataset.sessionId;
if (targetId === draggedId) return; if (targetId === draggedId) return;
// Reorder // Reorder
const fromIdx = this.order.indexOf(draggedId); const fromIdx = this.order.indexOf(draggedId);
const toIdx = this.order.indexOf(targetId); const toIdx = this.order.indexOf(targetId);
if (fromIdx === -1 || toIdx === -1) return; if (fromIdx === -1 || toIdx === -1) return;
this.order.splice(fromIdx, 1); this.order.splice(fromIdx, 1);
this.order.splice(toIdx, 0, draggedId); this.order.splice(toIdx, 0, draggedId);
this._reorderDOM(); this._reorderDOM();
}); });
this.tabList.addEventListener('dragend', () => { this.tabList.addEventListener("dragend", () => {
this.tabList.querySelectorAll('.tab').forEach((t) => { this.tabList.querySelectorAll(".tab").forEach((t) => {
t.classList.remove('tab-dragging', 'tab-drop-before', 'tab-drop-after'); t.classList.remove(
}); "tab-dragging",
draggedId = null; "tab-drop-before",
"tab-drop-after",
);
}); });
draggedId = null;
});
} }
} }
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.TabManager = TabManager;
window.NexusTerminal = window.NexusTerminal || {};
window.NexusTerminal.TabManager = TabManager;
})(); })();