feat: add layout switcher with grid preview supporting 1x1 to 4x4 layouts
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-title">XCTerminal</div>
|
||||
<div class="terminal-controls">
|
||||
<button id="layout-toggle" title="Layout">⊞</button>
|
||||
<button id="theme-toggle" title="Adjust Brightness">☀</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,10 +23,199 @@
|
||||
<span class="brightness-icon">☀</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="layout-popup" class="layout-popup hidden">
|
||||
<div class="layout-grid">
|
||||
<div class="layout-row">
|
||||
<div class="layout-option" data-cols="1" data-rows="1">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(1, 1fr); grid-template-rows: repeat(1, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">1×1</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="2" data-rows="1">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(1, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">1×2</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="3" data-rows="1">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(1, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">1×3</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="4" data-rows="1">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(1, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">1×4</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-row">
|
||||
<div class="layout-option" data-cols="1" data-rows="2">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(1, 1fr); grid-template-rows: repeat(2, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">2×1</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="2" data-rows="2">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">2×2</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="3" data-rows="2">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">2×3</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="4" data-rows="2">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">2×4</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-row">
|
||||
<div class="layout-option" data-cols="1" data-rows="3">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(1, 1fr); grid-template-rows: repeat(3, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">3×1</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="2" data-rows="3">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(3, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">3×2</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="3" data-rows="3">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">3×3</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="4" data-rows="3">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(3, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">3×4</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-row">
|
||||
<div class="layout-option" data-cols="1" data-rows="4">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(1, 1fr); grid-template-rows: repeat(4, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">4×1</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="2" data-rows="4">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(4, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">4×2</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="3" data-rows="4">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">4×3</span>
|
||||
</div>
|
||||
<div class="layout-option" data-cols="4" data-rows="4">
|
||||
<div class="layout-preview" style="grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr);">
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
<div class="layout-cell"></div>
|
||||
</div>
|
||||
<span class="layout-label">4×4</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -34,15 +224,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 terminalTop = document.getElementById('terminal-top');
|
||||
const terminalBottom = document.getElementById('terminal-bottom');
|
||||
const terminalsContainer = document.getElementById('terminals-container');
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const shell = urlParams.get('shell') || (window.navigator.platform.startsWith('Win') ? 'powershell.exe' : 'bash');
|
||||
|
||||
let ws1, ws2;
|
||||
let term1, term2;
|
||||
let fitAddon1, fitAddon2;
|
||||
let terminals = [];
|
||||
let websockets = [];
|
||||
let fitAddons = [];
|
||||
let currentLayout = { cols: 1, rows: 1 };
|
||||
let currentBrightness = 0;
|
||||
|
||||
function getThemeForBrightness(brightness) {
|
||||
@@ -109,12 +299,11 @@
|
||||
brightWhite: '#ffffff'
|
||||
};
|
||||
|
||||
if (term1) {
|
||||
term1.options.theme = terminalTheme;
|
||||
}
|
||||
if (term2) {
|
||||
term2.options.theme = terminalTheme;
|
||||
}
|
||||
terminals.forEach(term => {
|
||||
if (term) {
|
||||
term.options.theme = terminalTheme;
|
||||
}
|
||||
});
|
||||
|
||||
return { bgColor, headerBg, borderColor };
|
||||
}
|
||||
@@ -165,6 +354,104 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setupLayoutControl() {
|
||||
const layoutToggle = document.getElementById('layout-toggle');
|
||||
const layoutPopup = document.getElementById('layout-popup');
|
||||
const layoutOptions = document.querySelectorAll('.layout-option');
|
||||
|
||||
layoutToggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
layoutPopup.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
layoutOptions.forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
const cols = parseInt(option.dataset.cols);
|
||||
const rows = parseInt(option.dataset.rows);
|
||||
switchLayout(cols, rows);
|
||||
layoutPopup.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!layoutPopup.contains(e.target) && e.target !== layoutToggle) {
|
||||
layoutPopup.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function switchLayout(cols, rows) {
|
||||
currentLayout = { cols, rows };
|
||||
|
||||
terminals.forEach(term => {
|
||||
if (term) {
|
||||
term.dispose();
|
||||
}
|
||||
});
|
||||
websockets.forEach(ws => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
terminals = [];
|
||||
websockets = [];
|
||||
fitAddons = [];
|
||||
|
||||
terminalsContainer.innerHTML = '';
|
||||
terminalsContainer.style.display = 'grid';
|
||||
terminalsContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
|
||||
terminalsContainer.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
|
||||
terminalsContainer.style.gap = '4px';
|
||||
terminalsContainer.style.padding = '4px';
|
||||
terminalsContainer.style.background = 'var(--bg-primary)';
|
||||
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}?shell=${encodeURIComponent(shell)}`;
|
||||
|
||||
const totalTerminals = cols * rows;
|
||||
for (let i = 0; i < totalTerminals; i++) {
|
||||
const pane = document.createElement('div');
|
||||
pane.className = 'terminal-pane';
|
||||
pane.style.background = 'var(--bg-primary)';
|
||||
pane.style.padding = '4px';
|
||||
pane.style.overflow = 'hidden';
|
||||
terminalsContainer.appendChild(pane);
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
websockets.push(ws);
|
||||
const { term, fitAddon } = createTerminal(pane, ws);
|
||||
terminals.push(term);
|
||||
fitAddons.push(fitAddon);
|
||||
} catch (err) {
|
||||
console.error('Failed to create WebSocket:', err);
|
||||
terminals.push(null);
|
||||
websockets.push(null);
|
||||
fitAddons.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.layout-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
if (parseInt(opt.dataset.cols) === cols && parseInt(opt.dataset.rows) === rows) {
|
||||
opt.classList.add('selected');
|
||||
}
|
||||
});
|
||||
|
||||
if (terminals.length > 0 && terminals[0]) {
|
||||
terminals[0].focus();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
fitAddons.forEach(fitAddon => {
|
||||
if (fitAddon) {
|
||||
fitAddon.fit();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTerminal(container, ws) {
|
||||
const term = new Terminal({
|
||||
cursorBlink: true,
|
||||
@@ -253,36 +540,9 @@
|
||||
return { term, fitAddon };
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
setupLayoutControl();
|
||||
setupBrightnessControl();
|
||||
connectTerminal();
|
||||
switchLayout(1, 2);
|
||||
updateBrightness(0);
|
||||
initGhosttyVT();
|
||||
</script>
|
||||
|
||||
@@ -206,4 +206,93 @@ html, body {
|
||||
border: 2px solid #333;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#layout-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
#layout-toggle:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.layout-popup {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
right: 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.layout-popup.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.layout-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.layout-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.layout-option:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.layout-option.selected {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.layout-preview {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.layout-cell {
|
||||
background: var(--text-secondary);
|
||||
border-radius: 2px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.layout-option.selected .layout-cell {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.layout-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
Reference in New Issue
Block a user