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:
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+248
-242
@@ -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
@@ -1659,7 +1659,9 @@
|
|||||||
<button class="btn btn-icon btn-danger jump-host-remove" data-index="${i}" title="Remove">×</button>
|
<button class="btn btn-icon btn-danger jump-host-remove" data-index="${i}" title="Remove">×</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
@@ -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">×</button>
|
<button class="btn btn-icon btn-xs modal-close">×</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;
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user