Files
wterm/static/index.html
T
wt ee29bfe525
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-Make-Without-sqlite3 (push) Successful in 5s
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-CMake-Without-sqlite3 (push) Successful in 9s
Prepare Build and Testing executable binary / Prepare-Build-Testing-With-Meson-Without-sqlite3 (push) Successful in 7s
updated added tabs and closing tabs
2025-11-18 13:01:34 +07:00

715 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html>
<head>
<title>Web Terminal with Tabs</title>
<link rel="stylesheet" href="xterm.css" />
<style>
/* Стили остаются без изменений */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
background-color: #1e1e1e;
color: #ffffff;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
background-color: #2d2d2d;
padding: 15px 20px;
border-bottom: 1px solid #444;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.header h1 {
font-size: 1.5em;
font-weight: 300;
}
.tabs-container {
background-color: #252526;
border-bottom: 1px solid #444;
display: flex;
align-items: center;
padding: 0 10px;
flex-shrink: 0;
min-height: 48px;
}
.tabs-list {
display: flex;
flex: 1;
overflow-x: auto;
}
.tab {
background-color: #2d2d2d;
color: #cccccc;
padding: 10px 20px;
border: 1px solid #444;
border-bottom: none;
cursor: pointer;
display: flex;
align-items: center;
min-width: 120px;
max-width: 200px;
position: relative;
margin-right: 2px;
}
.tab.active {
background-color: #1e1e1e;
color: #ffffff;
border-top: 2px solid #007acc;
}
.tab-title {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-close {
margin-left: 8px;
padding: 2px;
border-radius: 3px;
cursor: pointer;
opacity: 0.7;
}
.tab-close:hover {
background-color: #444;
opacity: 1;
}
.add-tab {
background: none;
border: none;
color: #cccccc;
font-size: 18px;
cursor: pointer;
padding: 8px 12px;
margin-left: 5px;
border-radius: 3px;
}
.add-tab:hover {
background-color: #444;
}
.terminals-container {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.terminal-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
background-color: #000000;
}
.terminal-wrapper.active {
display: block;
}
.terminal {
width: 100%;
height: 100%;
padding: 10px;
}
.status-bar {
background-color: #007acc;
color: white;
padding: 5px 15px;
font-size: 12px;
display: flex;
justify-content: space-between;
flex-shrink: 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #888;
text-align: center;
}
.empty-state h2 {
margin-bottom: 10px;
font-weight: 300;
}
.btn {
background-color: #007acc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 3px;
cursor: pointer;
margin-top: 15px;
}
.btn:hover {
background-color: #005a9e;
}
.connection-status {
font-size: 14px;
padding: 4px 8px;
border-radius: 3px;
}
.status-connected {
background-color: #2d5a2d;
}
.status-disconnected {
background-color: #5a2d2d;
}
.status-connecting {
background-color: #5a5a2d;
}
/* Scrollbar styling */
.tabs-list::-webkit-scrollbar {
height: 6px;
}
.tabs-list::-webkit-scrollbar-track {
background: #2d2d2d;
}
.tabs-list::-webkit-scrollbar-thumb {
background: #555;
border-radius: 3px;
}
.tabs-list::-webkit-scrollbar-thumb:hover {
background: #777;
}
</style>
</head>
<body>
<div class="header">
<h1>Web Terminal</h1>
<div class="connection-status status-connecting" id="global-status">
Initializing...
</div>
</div>
<div class="tabs-container">
<div class="tabs-list" id="tabs-list">
<!-- Tabs will be dynamically added here -->
</div>
<button class="add-tab" id="add-tab" title="New Terminal">+</button>
</div>
<div class="terminals-container" id="terminals-container">
<div class="empty-state" id="empty-state">
<h2>No terminals open</h2>
<p>Create a new terminal tab to get started</p>
<button class="btn" id="first-terminal-btn">
New Terminal
</button>
</div>
<!-- Terminal wrappers will be dynamically added here -->
</div>
<div class="status-bar">
<span id="active-terminal-info">No active terminal</span>
<span
class="connection-status status-disconnected"
id="server-status"
>Disconnected</span
>
</div>
<script src="xterm.js"></script>
<script src="xterm-addon-fit.js"></script>
<script>
class TabManager {
constructor() {
this.tabs = new Map(); // Map: tabId -> { terminal, ws, container, tabElement }
this.activeTabId = null;
this.nextTabId = 1;
this.tabsList = document.getElementById("tabs-list");
this.terminalsContainer = document.getElementById(
"terminals-container",
);
this.emptyState = document.getElementById("empty-state");
this.addTabBtn = document.getElementById("add-tab");
this.firstTerminalBtn =
document.getElementById("first-terminal-btn");
this.activeTerminalInfo = document.getElementById(
"active-terminal-info",
);
this.serverStatus =
document.getElementById("server-status");
this.globalStatus =
document.getElementById("global-status");
this.init();
}
init() {
this.addTabBtn.addEventListener("click", () =>
this.createTab(),
);
this.firstTerminalBtn.addEventListener("click", () =>
this.createTab(),
);
// Create first tab automatically
setTimeout(() => {
this.createTab();
}, 100);
}
createTab() {
const tabId = `tab-${this.nextTabId++}`;
const tabTitle = `Terminal ${this.nextTabId - 1}`;
// Create tab element
const tabElement = document.createElement("div");
tabElement.className = "tab";
tabElement.dataset.tabId = tabId;
const titleSpan = document.createElement("span");
titleSpan.className = "tab-title";
titleSpan.textContent = `○ ${tabTitle}`;
const closeBtn = document.createElement("span");
closeBtn.className = "tab-close";
closeBtn.innerHTML = "×";
closeBtn.title = "Close terminal";
tabElement.appendChild(titleSpan);
tabElement.appendChild(closeBtn);
// Create terminal wrapper
const terminalWrapper = document.createElement("div");
terminalWrapper.className = "terminal-wrapper";
terminalWrapper.id = `terminal-${tabId}`;
terminalWrapper.dataset.tabId = tabId;
const terminalElement = document.createElement("div");
terminalElement.className = "terminal";
terminalWrapper.appendChild(terminalElement);
// Add to DOM
this.tabsList.appendChild(tabElement);
this.terminalsContainer.appendChild(terminalWrapper);
// Hide empty state
this.emptyState.style.display = "none";
// Initialize terminal
const terminal = new Terminal({
cursorBlink: true,
theme: {
background: "#000000",
foreground: "#ffffff",
cursor: "#ffffff",
selection: "#ffffff40",
},
});
const fitAddon = new FitAddon.FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(terminalElement);
fitAddon.fit();
// Store tab data with initial state
const tabData = {
terminal,
fitAddon,
ws: null,
container: terminalWrapper,
tabElement,
title: tabTitle,
connected: false,
connecting: true,
};
this.tabs.set(tabId, tabData);
// Update global status immediately
this.updateGlobalStatus();
// Create WebSocket connection
this.createWebSocket(tabId, terminal, tabData);
// Set up event listeners
tabElement.addEventListener("click", (e) => {
if (e.target !== closeBtn) {
this.switchToTab(tabId);
}
});
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.closeTab(tabId);
});
terminal.onData((data) => {
if (
tabData.ws &&
tabData.ws.readyState === WebSocket.OPEN
) {
tabData.ws.send(data);
}
});
// Switch to new tab
this.switchToTab(tabId);
// Handle resize
window.addEventListener("resize", () => {
if (this.activeTabId === tabId) {
fitAddon.fit();
this.sendResize(tabId);
}
});
return tabId;
}
createWebSocket(tabId, terminal, tabData) {
const protocol =
window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws`;
const ws = new WebSocket(wsUrl);
tabData.ws = ws;
ws.onopen = () => {
console.log(`WebSocket connected for tab ${tabId}`);
tabData.connected = true;
tabData.connecting = false;
this.updateTabStatus(tabId, true);
this.updateGlobalStatus();
this.sendResize(tabId);
// Update server status if this is the active tab
if (this.activeTabId === tabId) {
this.updateServerStatus(
"Connected",
"status-connected",
);
}
};
ws.onmessage = (event) => {
// Проверяем специальное сообщение о закрытии вкладки
if (event.data === "CLOSE_TAB") {
console.log(
`Received CLOSE_TAB message for tab ${tabId}`,
);
this.closeTab(tabId);
return;
}
if (typeof event.data === "string") {
terminal.write(event.data);
} else {
// Handle binary data
const reader = new FileReader();
reader.onload = () => {
const data = new Uint8Array(reader.result);
terminal.write(data);
};
reader.readAsArrayBuffer(event.data);
}
};
ws.onclose = (event) => {
console.log(
`WebSocket disconnected for tab ${tabId}:`,
event.code,
event.reason,
);
tabData.connected = false;
tabData.connecting = false;
this.updateTabStatus(tabId, false);
this.updateGlobalStatus();
if (this.activeTabId === tabId) {
this.updateServerStatus(
"Disconnected",
"status-disconnected",
);
}
// Auto-reconnect after delay, только если это не было закрытие по exit
if (event.code !== 1000) {
// 1000 - нормальное закрытие
setTimeout(() => {
if (
this.tabs.has(tabId) &&
!tabData.connected
) {
console.log(
`Attempting to reconnect tab ${tabId}`,
);
tabData.connecting = true;
this.updateTabStatus(tabId, false);
this.updateGlobalStatus();
this.createWebSocket(
tabId,
terminal,
tabData,
);
}
}, 3000);
}
};
ws.onerror = (error) => {
console.error(
`WebSocket error for tab ${tabId}:`,
error,
);
tabData.connected = false;
tabData.connecting = false;
this.updateTabStatus(tabId, false);
this.updateGlobalStatus();
if (this.activeTabId === tabId) {
this.updateServerStatus(
"Connection Error",
"status-disconnected",
);
}
};
}
switchToTab(tabId) {
if (!this.tabs.has(tabId)) return;
// Update active states
this.tabs.forEach((data, id) => {
const isActive = id === tabId;
data.tabElement.classList.toggle("active", isActive);
data.container.classList.toggle("active", isActive);
});
this.activeTabId = tabId;
// Fit terminal to container
const tabData = this.tabs.get(tabId);
setTimeout(() => {
tabData.fitAddon.fit();
this.sendResize(tabId);
}, 10);
// Update status bar
this.updateActiveTerminalInfo(tabId);
// Update server status based on tab connection state
if (tabData.connected) {
this.updateServerStatus(
"Connected",
"status-connected",
);
} else if (tabData.connecting) {
this.updateServerStatus(
"Connecting...",
"status-connecting",
);
} else {
this.updateServerStatus(
"Disconnected",
"status-disconnected",
);
}
}
closeTab(tabId) {
if (!this.tabs.has(tabId)) return;
const tabData = this.tabs.get(tabId);
// Close WebSocket
if (tabData.ws) {
tabData.ws.close(1000, "Tab closed"); // 1000 - нормальное закрытие
}
// Destroy terminal
tabData.terminal.dispose();
// Remove from DOM
tabData.tabElement.remove();
tabData.container.remove();
// Remove from tabs map
this.tabs.delete(tabId);
// If this was the active tab, switch to another
if (this.activeTabId === tabId) {
if (this.tabs.size > 0) {
// Switch to the last tab in the list
const lastTabId = Array.from(this.tabs.keys())[
this.tabs.size - 1
];
this.switchToTab(lastTabId);
} else {
this.activeTabId = null;
this.emptyState.style.display = "flex";
this.updateActiveTerminalInfo(null);
this.updateServerStatus(
"Disconnected",
"status-disconnected",
);
}
}
// Update global status
this.updateGlobalStatus();
}
sendResize(tabId) {
const tabData = this.tabs.get(tabId);
if (
tabData &&
tabData.ws &&
tabData.ws.readyState === WebSocket.OPEN
) {
const dimensions = tabData.terminal;
const resizeMsg = {
cols: dimensions.cols,
rows: dimensions.rows,
};
tabData.ws.send(JSON.stringify(resizeMsg));
}
}
updateTabStatus(tabId, connected) {
const tabData = this.tabs.get(tabId);
if (tabData) {
const statusIndicator = connected
? "●"
: tabData.connecting
? "⟳"
: "○";
const originalTitle = tabData.title.replace(
/[●○⟳] /,
"",
);
tabData.tabElement.querySelector(
".tab-title",
).textContent = `${statusIndicator} ${originalTitle}`;
if (this.activeTabId === tabId) {
if (connected) {
this.updateServerStatus(
"Connected",
"status-connected",
);
} else if (tabData.connecting) {
this.updateServerStatus(
"Connecting...",
"status-connecting",
);
} else {
this.updateServerStatus(
"Disconnected",
"status-disconnected",
);
}
}
}
}
updateActiveTerminalInfo(tabId) {
if (!tabId) {
this.activeTerminalInfo.textContent =
"No active terminal";
return;
}
const tabData = this.tabs.get(tabId);
if (tabData) {
const originalTitle = tabData.title.replace(
/[●○⟳] /,
"",
);
this.activeTerminalInfo.textContent = `Active: ${originalTitle}`;
}
}
updateServerStatus(text, className) {
this.serverStatus.textContent = text;
this.serverStatus.className = `connection-status ${className}`;
}
updateGlobalStatus() {
const totalTabs = this.tabs.size;
if (totalTabs === 0) {
this.globalStatus.textContent = "No terminals";
this.globalStatus.className =
"connection-status status-disconnected";
return;
}
const connectedTabs = Array.from(this.tabs.values()).filter(
(tab) => tab.connected,
).length;
const connectingTabs = Array.from(
this.tabs.values(),
).filter((tab) => tab.connecting).length;
if (connectedTabs === totalTabs) {
// All tabs connected
this.globalStatus.textContent = `${connectedTabs}/${totalTabs} connected`;
this.globalStatus.className =
"connection-status status-connected";
} else if (connectedTabs === 0 && connectingTabs === 0) {
// All tabs disconnected
this.globalStatus.textContent = `${connectedTabs}/${totalTabs} connected`;
this.globalStatus.className =
"connection-status status-disconnected";
} else {
// Some tabs connecting or mixed state
this.globalStatus.textContent = `${connectedTabs}/${totalTabs} connected`;
this.globalStatus.className =
"connection-status status-connecting";
}
}
}
// Initialize tab manager when page loads
document.addEventListener("DOMContentLoaded", () => {
new TabManager();
});
</script>
</body>
</html>