feat: add vertical split layout with two independent terminals
This commit is contained in:
@@ -22,11 +22,10 @@
|
||||
<span class="brightness-icon">☀</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
<div class="terminal-status">
|
||||
<span id="connection-status">Connecting...</span>
|
||||
<span id="shell-name"></span>
|
||||
<span id="wasm-status"></span>
|
||||
<div id="terminals-container">
|
||||
<div class="terminal-pane" id="terminal-top"></div>
|
||||
<div class="terminal-divider"></div>
|
||||
<div class="terminal-pane" id="terminal-bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,17 +34,15 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0.16.0/lib/addon-webgl.min.js"></script>
|
||||
<script src="ghostty-vt.js"></script>
|
||||
<script>
|
||||
const terminalContainer = document.getElementById('terminal');
|
||||
const connectionStatus = document.getElementById('connection-status');
|
||||
const shellName = document.getElementById('shell-name');
|
||||
const wasmStatus = document.getElementById('wasm-status');
|
||||
const terminalTop = document.getElementById('terminal-top');
|
||||
const terminalBottom = document.getElementById('terminal-bottom');
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const shell = urlParams.get('shell') || (window.navigator.platform.startsWith('Win') ? 'powershell.exe' : 'bash');
|
||||
shellName.textContent = shell;
|
||||
|
||||
let ws;
|
||||
let term;
|
||||
let ws1, ws2;
|
||||
let term1, term2;
|
||||
let fitAddon1, fitAddon2;
|
||||
let currentBrightness = 0;
|
||||
|
||||
function getThemeForBrightness(brightness) {
|
||||
@@ -112,21 +109,22 @@
|
||||
brightWhite: '#ffffff'
|
||||
};
|
||||
|
||||
if (term) {
|
||||
term.options.theme = terminalTheme;
|
||||
if (term1) {
|
||||
term1.options.theme = terminalTheme;
|
||||
}
|
||||
if (term2) {
|
||||
term2.options.theme = terminalTheme;
|
||||
}
|
||||
|
||||
return { bgColor, headerBg, statusBg, textColor, borderColor };
|
||||
return { bgColor, headerBg, borderColor };
|
||||
}
|
||||
|
||||
function updateBrightness(brightness) {
|
||||
currentBrightness = brightness;
|
||||
const { bgColor, headerBg, statusBg, borderColor } = getThemeForBrightness(brightness);
|
||||
const { bgColor, headerBg, borderColor } = getThemeForBrightness(brightness);
|
||||
document.body.style.background = bgColor;
|
||||
document.querySelector('.terminal-header').style.background = headerBg;
|
||||
document.querySelector('.terminal-header').style.borderBottomColor = borderColor;
|
||||
document.querySelector('.terminal-status').style.background = statusBg;
|
||||
document.querySelector('.terminal-status').style.borderTopColor = borderColor;
|
||||
|
||||
const sliderThumb = document.querySelector('#brightness-slider::-webkit-slider-thumb');
|
||||
if (sliderThumb) {
|
||||
@@ -157,35 +155,18 @@
|
||||
|
||||
async function initGhosttyVT() {
|
||||
const wasmUrl = urlParams.get('wasm') || 'ghostty-vt.wasm';
|
||||
wasmStatus.textContent = 'Loading libghostty-vt...';
|
||||
|
||||
const loaded = await GhosttyVT.loadWASM(wasmUrl);
|
||||
|
||||
if (loaded) {
|
||||
wasmStatus.textContent = 'libghostty-vt: Active';
|
||||
wasmStatus.classList.add('active');
|
||||
console.log('libghostty-vt: Active');
|
||||
} else {
|
||||
wasmStatus.textContent = 'libghostty-vt: Fallback (xterm.js)';
|
||||
wasmStatus.classList.add('fallback');
|
||||
console.log('libghostty-vt: Fallback (xterm.js)');
|
||||
}
|
||||
}
|
||||
|
||||
function connectTerminal() {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}?shell=${encodeURIComponent(shell)}`;
|
||||
|
||||
connectionStatus.textContent = 'Connecting...';
|
||||
terminalContainer.classList.add('connecting');
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to create WebSocket:', err);
|
||||
connectionStatus.textContent = 'Connection failed';
|
||||
return;
|
||||
}
|
||||
|
||||
term = new Terminal({
|
||||
function createTerminal(container, ws) {
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'block',
|
||||
fontSize: 14,
|
||||
@@ -229,8 +210,7 @@
|
||||
console.log('WebGL not available, using Canvas renderer');
|
||||
}
|
||||
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(terminalContainer);
|
||||
term.open(container);
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
@@ -241,15 +221,6 @@
|
||||
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
connectionStatus.textContent = 'Connected';
|
||||
connectionStatus.classList.remove('connecting');
|
||||
connectionStatus.classList.add('connected');
|
||||
terminalContainer.classList.remove('connecting');
|
||||
terminalContainer.classList.add('connected');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const text = new TextDecoder().decode(event.data);
|
||||
@@ -259,18 +230,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket closed:', event.code, event.reason);
|
||||
connectionStatus.textContent = `Disconnected (${event.code})`;
|
||||
connectionStatus.classList.remove('connected');
|
||||
connectionStatus.classList.add('disconnected');
|
||||
terminalContainer.classList.remove('connected');
|
||||
ws.onclose = () => {
|
||||
term.write('\r\n\x1b[33m[Connection closed]\x1b[0m\r\n');
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
connectionStatus.textContent = 'Error';
|
||||
};
|
||||
|
||||
term.onData((data) => {
|
||||
@@ -285,11 +250,35 @@
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
fitAddon.fit();
|
||||
});
|
||||
return { term, fitAddon };
|
||||
}
|
||||
|
||||
term.focus();
|
||||
function connectTerminal() {
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}?shell=${encodeURIComponent(shell)}`;
|
||||
|
||||
try {
|
||||
ws1 = new WebSocket(wsUrl);
|
||||
ws2 = new WebSocket(wsUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to create WebSocket:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const result1 = createTerminal(terminalTop, ws1);
|
||||
const result2 = createTerminal(terminalBottom, ws2);
|
||||
|
||||
term1 = result1.term;
|
||||
term2 = result2.term;
|
||||
fitAddon1 = result1.fitAddon;
|
||||
fitAddon2 = result2.fitAddon;
|
||||
|
||||
term1.focus();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
fitAddon1.fit();
|
||||
fitAddon2.fit();
|
||||
});
|
||||
}
|
||||
|
||||
setupBrightnessControl();
|
||||
|
||||
@@ -79,20 +79,34 @@ html, body {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
#terminal {
|
||||
#terminals-container {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
#terminal.connecting {
|
||||
opacity: 0.6;
|
||||
.terminal-pane {
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm {
|
||||
.terminal-pane .xterm {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.terminal-divider {
|
||||
height: 4px;
|
||||
background: var(--bg-secondary);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.terminal-divider:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.xterm-viewport::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
@@ -106,57 +120,13 @@ html, body {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.terminal-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#connection-status.connecting {
|
||||
color: #f0a050;
|
||||
}
|
||||
|
||||
#connection-status.connected {
|
||||
color: #50c878;
|
||||
}
|
||||
|
||||
#connection-status.connected::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #50c878;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
#connection-status.disconnected {
|
||||
color: #d35252;
|
||||
}
|
||||
|
||||
#wasm-status.active {
|
||||
color: #50c878;
|
||||
}
|
||||
|
||||
#wasm-status.fallback {
|
||||
color: #f0a050;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.terminal-header {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.terminal-status {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
padding: 4px;
|
||||
.terminal-pane {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user