x-shell.js 0.1.1 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -3
- package/dist/client/browser-bundle.js +43 -1
- package/dist/client/browser-bundle.js.map +2 -2
- package/dist/client/terminal-client.d.ts +20 -1
- package/dist/client/terminal-client.d.ts.map +1 -1
- package/dist/client/terminal-client.js +43 -0
- package/dist/client/terminal-client.js.map +1 -1
- package/dist/server/terminal-server.d.ts +25 -1
- package/dist/server/terminal-server.d.ts.map +1 -1
- package/dist/server/terminal-server.js +210 -25
- package/dist/server/terminal-server.js.map +1 -1
- package/dist/shared/types.d.ts +59 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/ui/browser-bundle.js +571 -15
- package/dist/ui/browser-bundle.js.map +3 -3
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/styles.js +22 -0
- package/dist/ui/styles.js.map +1 -1
- package/dist/ui/x-shell-terminal.d.ts +65 -2
- package/dist/ui/x-shell-terminal.d.ts.map +1 -1
- package/dist/ui/x-shell-terminal.js +536 -13
- package/dist/ui/x-shell-terminal.js.map +1 -1
- package/package.json +5 -3
|
@@ -37,6 +37,15 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
37
37
|
this.noHeader = false;
|
|
38
38
|
this.autoConnect = false;
|
|
39
39
|
this.autoSpawn = false;
|
|
40
|
+
// Docker container properties
|
|
41
|
+
this.container = '';
|
|
42
|
+
this.containerShell = '';
|
|
43
|
+
this.containerUser = '';
|
|
44
|
+
this.containerCwd = '';
|
|
45
|
+
// UI panel options
|
|
46
|
+
this.showConnectionPanel = false;
|
|
47
|
+
this.showSettings = false;
|
|
48
|
+
this.showStatusBar = false;
|
|
40
49
|
// Terminal appearance
|
|
41
50
|
this.fontSize = 14;
|
|
42
51
|
this.fontFamily = 'Menlo, Monaco, "Courier New", monospace';
|
|
@@ -49,6 +58,17 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
49
58
|
this.loading = false;
|
|
50
59
|
this.error = null;
|
|
51
60
|
this.sessionInfo = null;
|
|
61
|
+
// Connection panel state
|
|
62
|
+
this.containers = [];
|
|
63
|
+
this.serverInfo = null;
|
|
64
|
+
this.selectedContainer = '';
|
|
65
|
+
this.selectedShell = '/bin/sh';
|
|
66
|
+
this.connectionMode = 'docker';
|
|
67
|
+
// Settings state
|
|
68
|
+
this.settingsMenuOpen = false;
|
|
69
|
+
// Status bar state
|
|
70
|
+
this.statusMessage = '';
|
|
71
|
+
this.statusType = 'info';
|
|
52
72
|
// xterm.js module (loaded dynamically)
|
|
53
73
|
this.xtermModule = null;
|
|
54
74
|
this.fitAddonModule = null;
|
|
@@ -89,6 +109,31 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
89
109
|
throw new Error('Failed to load xterm.js. Make sure it is available.');
|
|
90
110
|
}
|
|
91
111
|
}
|
|
112
|
+
// Inject xterm CSS into shadow DOM (required because CSS doesn't cross shadow boundaries)
|
|
113
|
+
await this.injectXtermCSS();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Inject xterm.js CSS into shadow DOM
|
|
117
|
+
*/
|
|
118
|
+
async injectXtermCSS() {
|
|
119
|
+
if (!this.shadowRoot)
|
|
120
|
+
return;
|
|
121
|
+
// Check if already injected
|
|
122
|
+
if (this.shadowRoot.querySelector('#xterm-styles'))
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
// Fetch xterm CSS from CDN
|
|
126
|
+
const response = await fetch('https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css');
|
|
127
|
+
const css = await response.text();
|
|
128
|
+
// Create style element and inject into shadow DOM
|
|
129
|
+
const style = document.createElement('style');
|
|
130
|
+
style.id = 'xterm-styles';
|
|
131
|
+
style.textContent = css;
|
|
132
|
+
this.shadowRoot.prepend(style);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.warn('[x-shell] Failed to load xterm CSS:', e);
|
|
136
|
+
}
|
|
92
137
|
}
|
|
93
138
|
/**
|
|
94
139
|
* Connect to the terminal server
|
|
@@ -119,6 +164,7 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
119
164
|
});
|
|
120
165
|
this.client.onError((err) => {
|
|
121
166
|
this.error = err.message;
|
|
167
|
+
this.setStatus(err.message, 'error');
|
|
122
168
|
this.dispatchEvent(new CustomEvent('error', { detail: { error: err }, bubbles: true, composed: true }));
|
|
123
169
|
});
|
|
124
170
|
this.client.onData((data) => {
|
|
@@ -137,8 +183,24 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
137
183
|
});
|
|
138
184
|
this.client.onSpawned((info) => {
|
|
139
185
|
this.sessionInfo = info;
|
|
186
|
+
this.setStatus(`Session started: ${info.container || info.shell}`, 'success');
|
|
140
187
|
this.dispatchEvent(new CustomEvent('spawned', { detail: { session: info }, bubbles: true, composed: true }));
|
|
141
188
|
});
|
|
189
|
+
// Server info and container list handlers
|
|
190
|
+
this.client.onServerInfo((info) => {
|
|
191
|
+
this.serverInfo = info;
|
|
192
|
+
if (info.dockerEnabled) {
|
|
193
|
+
this.connectionMode = 'docker';
|
|
194
|
+
this.client?.requestContainerList();
|
|
195
|
+
}
|
|
196
|
+
this.selectedShell = info.defaultShell;
|
|
197
|
+
});
|
|
198
|
+
this.client.onContainerList((containers) => {
|
|
199
|
+
this.containers = containers;
|
|
200
|
+
if (containers.length > 0 && !this.selectedContainer) {
|
|
201
|
+
this.selectedContainer = containers[0].name;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
142
204
|
await this.client.connect();
|
|
143
205
|
}
|
|
144
206
|
catch (err) {
|
|
@@ -178,6 +240,11 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
178
240
|
cols: this.terminal?.cols || this.cols,
|
|
179
241
|
rows: this.terminal?.rows || this.rows,
|
|
180
242
|
env: options?.env,
|
|
243
|
+
// Docker container options
|
|
244
|
+
container: options?.container || this.container || undefined,
|
|
245
|
+
containerShell: options?.containerShell || this.containerShell || undefined,
|
|
246
|
+
containerUser: options?.containerUser || this.containerUser || undefined,
|
|
247
|
+
containerCwd: options?.containerCwd || this.containerCwd || undefined,
|
|
181
248
|
};
|
|
182
249
|
const info = await this.client.spawn(spawnOptions);
|
|
183
250
|
this.sessionActive = true;
|
|
@@ -186,9 +253,11 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
186
253
|
if (this.terminal) {
|
|
187
254
|
this.terminal.focus();
|
|
188
255
|
}
|
|
256
|
+
return info;
|
|
189
257
|
}
|
|
190
258
|
catch (err) {
|
|
191
259
|
this.error = err instanceof Error ? err.message : 'Failed to spawn session';
|
|
260
|
+
throw err;
|
|
192
261
|
}
|
|
193
262
|
finally {
|
|
194
263
|
this.loading = false;
|
|
@@ -251,21 +320,29 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
251
320
|
* Get terminal theme based on component theme
|
|
252
321
|
*/
|
|
253
322
|
getTerminalTheme() {
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
if (this.theme === '
|
|
323
|
+
// Determine effective theme (handle 'auto' by checking system preference)
|
|
324
|
+
let effectiveTheme = this.theme;
|
|
325
|
+
if (this.theme === 'auto') {
|
|
326
|
+
effectiveTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
327
|
+
}
|
|
328
|
+
if (effectiveTheme === 'light') {
|
|
257
329
|
return {
|
|
258
330
|
background: '#ffffff',
|
|
259
331
|
foreground: '#1f2937',
|
|
260
332
|
cursor: '#1f2937',
|
|
333
|
+
cursorAccent: '#ffffff',
|
|
261
334
|
selection: '#b4d5fe',
|
|
335
|
+
selectionForeground: '#1f2937',
|
|
262
336
|
};
|
|
263
337
|
}
|
|
338
|
+
// Dark theme
|
|
264
339
|
return {
|
|
265
340
|
background: '#1e1e1e',
|
|
266
341
|
foreground: '#cccccc',
|
|
267
342
|
cursor: '#ffffff',
|
|
343
|
+
cursorAccent: '#1e1e1e',
|
|
268
344
|
selection: '#264f78',
|
|
345
|
+
selectionForeground: '#ffffff',
|
|
269
346
|
};
|
|
270
347
|
}
|
|
271
348
|
/**
|
|
@@ -328,6 +405,263 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
328
405
|
}
|
|
329
406
|
this.fitAddon = null;
|
|
330
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Set status message
|
|
410
|
+
*/
|
|
411
|
+
setStatus(message, type = 'info') {
|
|
412
|
+
this.statusMessage = message;
|
|
413
|
+
this.statusType = type;
|
|
414
|
+
// Auto-clear success/info messages after 5 seconds
|
|
415
|
+
if (type !== 'error') {
|
|
416
|
+
setTimeout(() => {
|
|
417
|
+
if (this.statusMessage === message) {
|
|
418
|
+
this.statusMessage = '';
|
|
419
|
+
}
|
|
420
|
+
}, 5000);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Clear status message
|
|
425
|
+
*/
|
|
426
|
+
clearStatus() {
|
|
427
|
+
this.statusMessage = '';
|
|
428
|
+
this.statusType = 'info';
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Handle theme change
|
|
432
|
+
*/
|
|
433
|
+
handleThemeChange(e) {
|
|
434
|
+
const select = e.target;
|
|
435
|
+
this.theme = select.value;
|
|
436
|
+
// Apply theme to xterm.js terminal
|
|
437
|
+
this.applyTerminalTheme();
|
|
438
|
+
this.dispatchEvent(new CustomEvent('theme-change', {
|
|
439
|
+
detail: { theme: this.theme },
|
|
440
|
+
bubbles: true,
|
|
441
|
+
composed: true
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Apply current theme to xterm.js terminal
|
|
446
|
+
*/
|
|
447
|
+
applyTerminalTheme() {
|
|
448
|
+
if (!this.terminal)
|
|
449
|
+
return;
|
|
450
|
+
const terminalTheme = this.getTerminalTheme();
|
|
451
|
+
this.terminal.options.theme = terminalTheme;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Apply current font size to xterm.js terminal
|
|
455
|
+
*/
|
|
456
|
+
applyTerminalFontSize() {
|
|
457
|
+
if (!this.terminal)
|
|
458
|
+
return;
|
|
459
|
+
this.terminal.options.fontSize = this.fontSize;
|
|
460
|
+
// Re-fit the terminal after font size change
|
|
461
|
+
if (this.fitAddon) {
|
|
462
|
+
this.fitAddon.fit();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Handle connection mode change
|
|
467
|
+
*/
|
|
468
|
+
handleModeChange(e) {
|
|
469
|
+
const select = e.target;
|
|
470
|
+
this.connectionMode = select.value;
|
|
471
|
+
if (this.connectionMode === 'docker' && this.client && this.connected) {
|
|
472
|
+
this.client.requestContainerList();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Handle connect from connection panel
|
|
477
|
+
*/
|
|
478
|
+
async handlePanelConnect() {
|
|
479
|
+
if (!this.connected) {
|
|
480
|
+
await this.connect();
|
|
481
|
+
}
|
|
482
|
+
if (this.connected) {
|
|
483
|
+
const options = {};
|
|
484
|
+
if (this.connectionMode === 'docker' && this.selectedContainer) {
|
|
485
|
+
options.container = this.selectedContainer;
|
|
486
|
+
options.containerShell = this.selectedShell || '/bin/sh';
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
options.shell = this.selectedShell || undefined;
|
|
490
|
+
}
|
|
491
|
+
await this.spawn(options);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Toggle settings menu
|
|
496
|
+
*/
|
|
497
|
+
toggleSettingsMenu() {
|
|
498
|
+
this.settingsMenuOpen = !this.settingsMenuOpen;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Render connection panel
|
|
502
|
+
*/
|
|
503
|
+
renderConnectionPanel() {
|
|
504
|
+
if (!this.showConnectionPanel)
|
|
505
|
+
return nothing;
|
|
506
|
+
const runningContainers = this.containers.filter(c => c.state === 'running');
|
|
507
|
+
return html `
|
|
508
|
+
<div class="connection-panel">
|
|
509
|
+
<div class="connection-panel-title">
|
|
510
|
+
<span>Connection</span>
|
|
511
|
+
${this.serverInfo?.dockerEnabled
|
|
512
|
+
? html `<span style="font-size: 11px; color: var(--xs-status-connected);">Docker enabled</span>`
|
|
513
|
+
: nothing}
|
|
514
|
+
</div>
|
|
515
|
+
<div class="connection-form">
|
|
516
|
+
<div class="form-group">
|
|
517
|
+
<label>Mode</label>
|
|
518
|
+
<select
|
|
519
|
+
.value=${this.connectionMode}
|
|
520
|
+
@change=${this.handleModeChange}
|
|
521
|
+
?disabled=${this.sessionActive}
|
|
522
|
+
>
|
|
523
|
+
<option value="local">Local Shell</option>
|
|
524
|
+
${this.serverInfo?.dockerEnabled
|
|
525
|
+
? html `<option value="docker">Docker Container</option>`
|
|
526
|
+
: nothing}
|
|
527
|
+
</select>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
${this.connectionMode === 'docker' ? html `
|
|
531
|
+
<div class="form-group">
|
|
532
|
+
<label>Container</label>
|
|
533
|
+
<select
|
|
534
|
+
.value=${this.selectedContainer}
|
|
535
|
+
@change=${(e) => this.selectedContainer = e.target.value}
|
|
536
|
+
?disabled=${this.sessionActive}
|
|
537
|
+
>
|
|
538
|
+
${runningContainers.length === 0
|
|
539
|
+
? html `<option value="">No containers running</option>`
|
|
540
|
+
: runningContainers.map(c => html `
|
|
541
|
+
<option value=${c.name}>${c.name} (${c.image})</option>
|
|
542
|
+
`)}
|
|
543
|
+
</select>
|
|
544
|
+
</div>
|
|
545
|
+
` : nothing}
|
|
546
|
+
|
|
547
|
+
<div class="form-group">
|
|
548
|
+
<label>Shell</label>
|
|
549
|
+
<select
|
|
550
|
+
.value=${this.selectedShell}
|
|
551
|
+
@change=${(e) => this.selectedShell = e.target.value}
|
|
552
|
+
?disabled=${this.sessionActive}
|
|
553
|
+
>
|
|
554
|
+
${this.serverInfo?.allowedShells.length
|
|
555
|
+
? this.serverInfo.allowedShells.map(s => html `<option value=${s}>${s}</option>`)
|
|
556
|
+
: html `
|
|
557
|
+
<option value="/bin/bash">/bin/bash</option>
|
|
558
|
+
<option value="/bin/sh">/bin/sh</option>
|
|
559
|
+
<option value="/bin/zsh">/bin/zsh</option>
|
|
560
|
+
`}
|
|
561
|
+
</select>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<div class="form-group">
|
|
565
|
+
${!this.connected
|
|
566
|
+
? html `<button class="btn-primary" @click=${this.handlePanelConnect} ?disabled=${this.loading}>
|
|
567
|
+
${this.loading ? 'Connecting...' : 'Connect'}
|
|
568
|
+
</button>`
|
|
569
|
+
: !this.sessionActive
|
|
570
|
+
? html `<button class="btn-primary" @click=${this.handlePanelConnect} ?disabled=${this.loading}>
|
|
571
|
+
${this.loading ? 'Starting...' : 'Start Session'}
|
|
572
|
+
</button>`
|
|
573
|
+
: html `<button class="btn-danger" @click=${this.kill}>
|
|
574
|
+
Stop Session
|
|
575
|
+
</button>`}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
`;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Render settings dropdown
|
|
583
|
+
*/
|
|
584
|
+
renderSettingsDropdown() {
|
|
585
|
+
if (!this.showSettings)
|
|
586
|
+
return nothing;
|
|
587
|
+
return html `
|
|
588
|
+
<div class="settings-dropdown">
|
|
589
|
+
<button @click=${this.toggleSettingsMenu} title="Settings">
|
|
590
|
+
⚙️
|
|
591
|
+
</button>
|
|
592
|
+
${this.settingsMenuOpen ? html `
|
|
593
|
+
<div class="settings-menu">
|
|
594
|
+
<div class="settings-menu-item">
|
|
595
|
+
<span>Theme</span>
|
|
596
|
+
<select
|
|
597
|
+
.value=${this.theme}
|
|
598
|
+
@change=${this.handleThemeChange}
|
|
599
|
+
>
|
|
600
|
+
<option value="dark">Dark</option>
|
|
601
|
+
<option value="light">Light</option>
|
|
602
|
+
<option value="auto">Auto</option>
|
|
603
|
+
</select>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="settings-divider"></div>
|
|
606
|
+
<div class="settings-menu-item">
|
|
607
|
+
<span>Font Size</span>
|
|
608
|
+
<select
|
|
609
|
+
.value=${String(this.fontSize)}
|
|
610
|
+
@change=${(e) => {
|
|
611
|
+
this.fontSize = parseInt(e.target.value);
|
|
612
|
+
this.applyTerminalFontSize();
|
|
613
|
+
}}
|
|
614
|
+
>
|
|
615
|
+
<option value="12">12px</option>
|
|
616
|
+
<option value="14">14px</option>
|
|
617
|
+
<option value="16">16px</option>
|
|
618
|
+
<option value="18">18px</option>
|
|
619
|
+
</select>
|
|
620
|
+
</div>
|
|
621
|
+
<div class="settings-divider"></div>
|
|
622
|
+
<div class="settings-menu-item" @click=${this.clear}>
|
|
623
|
+
<span>Clear Terminal</span>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
` : nothing}
|
|
627
|
+
</div>
|
|
628
|
+
`;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Render status bar
|
|
632
|
+
*/
|
|
633
|
+
renderStatusBar() {
|
|
634
|
+
if (!this.showStatusBar)
|
|
635
|
+
return nothing;
|
|
636
|
+
return html `
|
|
637
|
+
<div class="status-bar">
|
|
638
|
+
<div class="status-bar-left">
|
|
639
|
+
<span class="status-dot ${this.connected ? 'connected' : ''}"></span>
|
|
640
|
+
<span>${this.connected
|
|
641
|
+
? (this.sessionActive ? 'Session active' : 'Connected')
|
|
642
|
+
: 'Disconnected'}</span>
|
|
643
|
+
${this.sessionInfo ? html `
|
|
644
|
+
<span style="color: var(--xs-text-muted)">|</span>
|
|
645
|
+
<span>${this.sessionInfo.container || this.sessionInfo.shell}</span>
|
|
646
|
+
<span style="color: var(--xs-text-muted)">${this.sessionInfo.cols}x${this.sessionInfo.rows}</span>
|
|
647
|
+
` : nothing}
|
|
648
|
+
</div>
|
|
649
|
+
<div class="status-bar-right">
|
|
650
|
+
${this.statusMessage ? html `
|
|
651
|
+
<span class="${this.statusType === 'error' ? 'status-bar-error' : this.statusType === 'success' ? 'status-bar-success' : ''}">
|
|
652
|
+
${this.statusType === 'error' ? '⚠️' : this.statusType === 'success' ? '✓' : ''}
|
|
653
|
+
${this.statusMessage}
|
|
654
|
+
</span>
|
|
655
|
+
<button
|
|
656
|
+
style="background: none; border: none; cursor: pointer; padding: 0; font-size: 10px;"
|
|
657
|
+
@click=${this.clearStatus}
|
|
658
|
+
title="Dismiss"
|
|
659
|
+
>✕</button>
|
|
660
|
+
` : nothing}
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
331
665
|
render() {
|
|
332
666
|
return html `
|
|
333
667
|
${this.noHeader
|
|
@@ -338,29 +672,38 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
338
672
|
<span>Terminal</span>
|
|
339
673
|
${this.sessionInfo
|
|
340
674
|
? html `<span style="font-weight: normal; font-size: 12px; color: var(--xs-text-muted)">
|
|
341
|
-
${this.sessionInfo.
|
|
675
|
+
${this.sessionInfo.container
|
|
676
|
+
? `${this.sessionInfo.container} (${this.sessionInfo.shell})`
|
|
677
|
+
: this.sessionInfo.shell}
|
|
342
678
|
</span>`
|
|
343
679
|
: nothing}
|
|
344
680
|
</div>
|
|
345
681
|
<div class="header-actions">
|
|
346
|
-
${!this.
|
|
682
|
+
${!this.showConnectionPanel ? html `
|
|
683
|
+
${!this.connected
|
|
347
684
|
? html `<button @click=${this.connect} ?disabled=${this.loading}>
|
|
348
|
-
|
|
349
|
-
|
|
685
|
+
${this.loading ? 'Connecting...' : 'Connect'}
|
|
686
|
+
</button>`
|
|
350
687
|
: !this.sessionActive
|
|
351
688
|
? html `<button @click=${() => this.spawn()} ?disabled=${this.loading}>
|
|
352
|
-
|
|
353
|
-
|
|
689
|
+
${this.loading ? 'Spawning...' : 'Start'}
|
|
690
|
+
</button>`
|
|
354
691
|
: html `<button @click=${this.kill}>Stop</button>`}
|
|
692
|
+
` : nothing}
|
|
355
693
|
<button @click=${this.clear} ?disabled=${!this.sessionActive}>Clear</button>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
<
|
|
359
|
-
|
|
694
|
+
${this.renderSettingsDropdown()}
|
|
695
|
+
${!this.showStatusBar ? html `
|
|
696
|
+
<div class="status">
|
|
697
|
+
<span class="status-dot ${this.connected ? 'connected' : ''}"></span>
|
|
698
|
+
<span>${this.connected ? 'Connected' : 'Disconnected'}</span>
|
|
699
|
+
</div>
|
|
700
|
+
` : nothing}
|
|
360
701
|
</div>
|
|
361
702
|
</div>
|
|
362
703
|
`}
|
|
363
704
|
|
|
705
|
+
${this.renderConnectionPanel()}
|
|
706
|
+
|
|
364
707
|
<div class="terminal-container">
|
|
365
708
|
${this.loading && !this.terminal
|
|
366
709
|
? html `<div class="loading"><span class="loading-spinner">⏳</span> Loading...</div>`
|
|
@@ -368,6 +711,8 @@ let XShellTerminal = class XShellTerminal extends LitElement {
|
|
|
368
711
|
? html `<div class="error">❌ ${this.error}</div>`
|
|
369
712
|
: nothing}
|
|
370
713
|
</div>
|
|
714
|
+
|
|
715
|
+
${this.renderStatusBar()}
|
|
371
716
|
`;
|
|
372
717
|
}
|
|
373
718
|
};
|
|
@@ -474,6 +819,139 @@ XShellTerminal.styles = [
|
|
|
474
819
|
:host([no-header]) .header {
|
|
475
820
|
display: none;
|
|
476
821
|
}
|
|
822
|
+
|
|
823
|
+
/* Connection panel */
|
|
824
|
+
.connection-panel {
|
|
825
|
+
padding: 12px;
|
|
826
|
+
background: var(--xs-bg-header);
|
|
827
|
+
border-bottom: 1px solid var(--xs-border);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.connection-panel-title {
|
|
831
|
+
font-weight: 600;
|
|
832
|
+
margin-bottom: 12px;
|
|
833
|
+
display: flex;
|
|
834
|
+
align-items: center;
|
|
835
|
+
gap: 8px;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.connection-form {
|
|
839
|
+
display: grid;
|
|
840
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
841
|
+
gap: 10px;
|
|
842
|
+
align-items: end;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.form-group {
|
|
846
|
+
display: flex;
|
|
847
|
+
flex-direction: column;
|
|
848
|
+
gap: 4px;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.form-group label {
|
|
852
|
+
font-size: 11px;
|
|
853
|
+
text-transform: uppercase;
|
|
854
|
+
color: var(--xs-text-muted);
|
|
855
|
+
letter-spacing: 0.5px;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.form-group select,
|
|
859
|
+
.form-group input {
|
|
860
|
+
padding: 6px 10px;
|
|
861
|
+
border: 1px solid var(--xs-border);
|
|
862
|
+
border-radius: 4px;
|
|
863
|
+
background: var(--xs-bg);
|
|
864
|
+
color: var(--xs-text);
|
|
865
|
+
font-size: 13px;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.form-group select:focus,
|
|
869
|
+
.form-group input:focus {
|
|
870
|
+
outline: none;
|
|
871
|
+
border-color: var(--xs-status-connected);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/* Settings dropdown */
|
|
875
|
+
.settings-dropdown {
|
|
876
|
+
position: relative;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.settings-menu {
|
|
880
|
+
position: absolute;
|
|
881
|
+
top: 100%;
|
|
882
|
+
right: 0;
|
|
883
|
+
margin-top: 4px;
|
|
884
|
+
min-width: 180px;
|
|
885
|
+
background: var(--xs-bg-header);
|
|
886
|
+
border: 1px solid var(--xs-border);
|
|
887
|
+
border-radius: 4px;
|
|
888
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
889
|
+
z-index: 100;
|
|
890
|
+
padding: 8px 0;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.settings-menu-item {
|
|
894
|
+
display: flex;
|
|
895
|
+
align-items: center;
|
|
896
|
+
justify-content: space-between;
|
|
897
|
+
padding: 8px 12px;
|
|
898
|
+
font-size: 13px;
|
|
899
|
+
cursor: pointer;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.settings-menu-item:hover {
|
|
903
|
+
background: var(--xs-btn-hover);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.settings-menu-item select {
|
|
907
|
+
padding: 4px 8px;
|
|
908
|
+
border: 1px solid var(--xs-border);
|
|
909
|
+
border-radius: 3px;
|
|
910
|
+
background: var(--xs-bg);
|
|
911
|
+
color: var(--xs-text);
|
|
912
|
+
font-size: 12px;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.settings-divider {
|
|
916
|
+
height: 1px;
|
|
917
|
+
background: var(--xs-border);
|
|
918
|
+
margin: 4px 0;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/* Status bar */
|
|
922
|
+
.status-bar {
|
|
923
|
+
display: flex;
|
|
924
|
+
align-items: center;
|
|
925
|
+
justify-content: space-between;
|
|
926
|
+
padding: 4px 12px;
|
|
927
|
+
background: var(--xs-bg-header);
|
|
928
|
+
border-top: 1px solid var(--xs-border);
|
|
929
|
+
font-size: 12px;
|
|
930
|
+
color: var(--xs-text-muted);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.status-bar-left {
|
|
934
|
+
display: flex;
|
|
935
|
+
align-items: center;
|
|
936
|
+
gap: 12px;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.status-bar-right {
|
|
940
|
+
display: flex;
|
|
941
|
+
align-items: center;
|
|
942
|
+
gap: 8px;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
.status-bar-error {
|
|
946
|
+
color: #ef4444;
|
|
947
|
+
display: flex;
|
|
948
|
+
align-items: center;
|
|
949
|
+
gap: 4px;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.status-bar-success {
|
|
953
|
+
color: var(--xs-status-connected);
|
|
954
|
+
}
|
|
477
955
|
`,
|
|
478
956
|
];
|
|
479
957
|
__decorate([
|
|
@@ -503,6 +981,27 @@ __decorate([
|
|
|
503
981
|
__decorate([
|
|
504
982
|
property({ type: Boolean, attribute: 'auto-spawn' })
|
|
505
983
|
], XShellTerminal.prototype, "autoSpawn", void 0);
|
|
984
|
+
__decorate([
|
|
985
|
+
property({ type: String })
|
|
986
|
+
], XShellTerminal.prototype, "container", void 0);
|
|
987
|
+
__decorate([
|
|
988
|
+
property({ type: String, attribute: 'container-shell' })
|
|
989
|
+
], XShellTerminal.prototype, "containerShell", void 0);
|
|
990
|
+
__decorate([
|
|
991
|
+
property({ type: String, attribute: 'container-user' })
|
|
992
|
+
], XShellTerminal.prototype, "containerUser", void 0);
|
|
993
|
+
__decorate([
|
|
994
|
+
property({ type: String, attribute: 'container-cwd' })
|
|
995
|
+
], XShellTerminal.prototype, "containerCwd", void 0);
|
|
996
|
+
__decorate([
|
|
997
|
+
property({ type: Boolean, attribute: 'show-connection-panel' })
|
|
998
|
+
], XShellTerminal.prototype, "showConnectionPanel", void 0);
|
|
999
|
+
__decorate([
|
|
1000
|
+
property({ type: Boolean, attribute: 'show-settings' })
|
|
1001
|
+
], XShellTerminal.prototype, "showSettings", void 0);
|
|
1002
|
+
__decorate([
|
|
1003
|
+
property({ type: Boolean, attribute: 'show-status-bar' })
|
|
1004
|
+
], XShellTerminal.prototype, "showStatusBar", void 0);
|
|
506
1005
|
__decorate([
|
|
507
1006
|
property({ type: Number, attribute: 'font-size' })
|
|
508
1007
|
], XShellTerminal.prototype, "fontSize", void 0);
|
|
@@ -533,6 +1032,30 @@ __decorate([
|
|
|
533
1032
|
__decorate([
|
|
534
1033
|
state()
|
|
535
1034
|
], XShellTerminal.prototype, "sessionInfo", void 0);
|
|
1035
|
+
__decorate([
|
|
1036
|
+
state()
|
|
1037
|
+
], XShellTerminal.prototype, "containers", void 0);
|
|
1038
|
+
__decorate([
|
|
1039
|
+
state()
|
|
1040
|
+
], XShellTerminal.prototype, "serverInfo", void 0);
|
|
1041
|
+
__decorate([
|
|
1042
|
+
state()
|
|
1043
|
+
], XShellTerminal.prototype, "selectedContainer", void 0);
|
|
1044
|
+
__decorate([
|
|
1045
|
+
state()
|
|
1046
|
+
], XShellTerminal.prototype, "selectedShell", void 0);
|
|
1047
|
+
__decorate([
|
|
1048
|
+
state()
|
|
1049
|
+
], XShellTerminal.prototype, "connectionMode", void 0);
|
|
1050
|
+
__decorate([
|
|
1051
|
+
state()
|
|
1052
|
+
], XShellTerminal.prototype, "settingsMenuOpen", void 0);
|
|
1053
|
+
__decorate([
|
|
1054
|
+
state()
|
|
1055
|
+
], XShellTerminal.prototype, "statusMessage", void 0);
|
|
1056
|
+
__decorate([
|
|
1057
|
+
state()
|
|
1058
|
+
], XShellTerminal.prototype, "statusType", void 0);
|
|
536
1059
|
XShellTerminal = __decorate([
|
|
537
1060
|
customElement('x-shell-terminal')
|
|
538
1061
|
], XShellTerminal);
|