Skip to content

Instantly share code, notes, and snippets.

@JensTech
Created March 27, 2026 18:48
Show Gist options
  • Select an option

  • Save JensTech/4c7a19e7b83999e7edc709f7373a8fb7 to your computer and use it in GitHub Desktop.

Select an option

Save JensTech/4c7a19e7b83999e7edc709f7373a8fb7 to your computer and use it in GitHub Desktop.
PHP server side terminal
<?php
// Simple PHP Terminal
// I would recommend you add a form of auth if you deploy this
// This publicly allows access to your sever without auth so
// lock it down on your server
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: text/plain');
$cmd = trim($_POST['cmd'] ?? '');
if ($cmd === '') {
echo '';
exit;
}
$output = shell_exec($cmd . ' 2>&1');
echo $output ?? '(no output)';
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>terminal</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0d0d0d;
--surface: #111;
--border: #1f1f1f;
--green: #00ff88;
--dim: #3a3a3a;
--text: #c8c8c8;
--error: #ff4f4f;
--tab-h: 32px;
}
html,
body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
height: 100vh;
}
#titlebar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--surface);
border-bottom: 1px solid var(--border);
user-select: none;
flex-shrink: 0;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.r {
background: #ff5f56;
cursor: pointer;
}
.dot.y {
background: #ffbd2e;
}
.dot.g {
background: #27c93f;
}
#titlebar-label {
margin-left: 8px;
color: var(--dim);
font-size: 11px;
letter-spacing: 0.08em;
}
#tabbar {
display: flex;
align-items: stretch;
background: var(--surface);
border-bottom: 1px solid var(--border);
height: var(--tab-h);
flex-shrink: 0;
overflow-x: auto;
}
#tabbar::-webkit-scrollbar {
display: none;
}
.tab {
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px;
cursor: pointer;
color: var(--dim);
font-size: 11px;
border-right: 1px solid var(--border);
white-space: nowrap;
transition: color 0.15s;
user-select: none;
}
.tab:hover {
color: var(--text);
}
.tab.active {
color: var(--green);
background: var(--bg);
}
.tab .tab-close {
opacity: 0;
font-size: 14px;
line-height: 1;
color: var(--dim);
transition: opacity 0.15s, color 0.15s;
padding: 0 2px;
}
.tab:hover .tab-close {
opacity: 1;
}
.tab .tab-close:hover {
color: #ff5f56;
}
#btn-new-tab {
display: flex;
align-items: center;
padding: 0 12px;
cursor: pointer;
color: var(--dim);
font-size: 18px;
line-height: 1;
transition: color 0.15s;
user-select: none;
flex-shrink: 0;
}
#btn-new-tab:hover {
color: var(--green);
}
#btn-new-tab.disabled {
color: var(--border);
cursor: not-allowed;
}
#split-container {
display: flex;
flex: 1;
overflow: hidden;
}
.pane {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
min-width: 0;
}
.pane+.pane {
border-left: 1px solid var(--border);
}
.pane-output {
flex: 1;
overflow-y: auto;
padding: 16px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-all;
}
.pane-output::-webkit-scrollbar {
width: 4px;
}
.pane-output::-webkit-scrollbar-track {
background: transparent;
}
.pane-output::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
.entry {
margin-bottom: 6px;
}
.prompt-line {
color: var(--green);
}
.prompt-line::before {
content: '$ ';
opacity: 0.5;
}
.result {
color: var(--text);
}
.result.empty {
color: var(--dim);
font-style: italic;
}
.disconnected-msg {
color: var(--dim);
padding: 8px 0;
}
.btn-reconnect {
background: none;
border: 1px solid var(--dim);
color: var(--green);
font-family: inherit;
font-size: 12px;
padding: 2px 10px;
border-radius: 3px;
cursor: pointer;
margin-left: 8px;
}
.resizer {
width: 4px;
background: var(--border);
cursor: col-resize;
flex-shrink: 0;
transition: background 0.15s;
}
.resizer:hover,
.resizer.dragging {
background: var(--dim);
}
.pane-inputbar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-top: 1px solid var(--border);
background: var(--surface);
flex-shrink: 0;
}
.pane-inputbar span {
color: var(--green);
opacity: 0.7;
white-space: nowrap;
}
.pane-cmd {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--green);
font-family: inherit;
font-size: 13px;
caret-color: var(--green);
}
.pane-cmd::placeholder {
color: var(--dim);
}
#btn-split {
margin-left: auto;
color: var(--dim);
font-size: 11px;
cursor: pointer;
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 3px;
transition: color 0.15s, border-color 0.15s;
user-select: none;
}
#btn-split:hover {
color: var(--green);
border-color: var(--dim);
}
#btn-split.disabled {
color: var(--border);
border-color: var(--border);
cursor: not-allowed;
}
#btn-popout {
color: var(--dim);
font-size: 11px;
cursor: pointer;
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 3px;
transition: color 0.15s, border-color 0.15s;
user-select: none;
margin-left: 6px;
}
#btn-popout:hover {
color: var(--green);
border-color: var(--dim);
}
body.popout #titlebar,
body.popout #tabbar {
display: none;
}
</style>
</head>
<body>
<div id="titlebar">
<div class="dot r" id="btn-kill" title="Disconnect active pane"></div>
<div class="dot y"></div>
<div class="dot g"></div>
<span id="titlebar-label"> <?= htmlspecialchars(php_uname('n')) ?> — bash </span>
<div id="btn-split" title="Split pane">⊞ split</div>
<div id="btn-popout" title="Pop out tab">⤢ popout</div>
</div>
<div id="tabbar">
<div id="btn-new-tab" title="New tab">+</div>
</div>
<div id="split-container"></div>
<script>
const MAX_TABS = 5;
const hostname = <?= json_encode(php_uname('n')) ?>;
const osinfo = <?= json_encode(php_uname('s') . ' ' . php_uname('r')) ?>;
let tabs = []; // { id, label, panes: [paneObj, ...] }
let activeTabId = null;
let tabCounter = 0;
let paneCounter = 0;
const tabbar = document.getElementById('tabbar');
const btnNewTab = document.getElementById('btn-new-tab');
const splitContainer = document.getElementById('split-container');
const btnSplit = document.getElementById('btn-split');
const btnKill = document.getElementById('btn-kill');
// window panes
function createPane() {
const id = ++paneCounter;
const history = [];
let histIdx = -1;
let killed = false;
const el = document.createElement('div');
el.className = 'pane';
el.dataset.paneId = id;
const output = document.createElement('div');
output.className = 'pane-output';
// welcome line
const welcome = document.createElement('div');
welcome.className = 'entry';
welcome.innerHTML = `
<div class="result">Connected to
<strong style="color:var(--green)">${hostname}</strong> · ${osinfo}
</div>`;
output.appendChild(welcome);
const inputbar = document.createElement('div');
inputbar.className = 'pane-inputbar';
const prompt = document.createElement('span');
prompt.textContent = '$';
const input = document.createElement('input');
input.className = 'pane-cmd';
input.type = 'text';
input.autocomplete = 'off';
input.spellcheck = false;
input.placeholder = 'type a command…';
inputbar.appendChild(prompt);
inputbar.appendChild(input);
el.appendChild(output);
el.appendChild(inputbar);
// disconnected msg (hidden by default)
const discMsg = document.createElement('div');
discMsg.className = 'disconnected-msg';
discMsg.style.display = 'none';
discMsg.innerHTML = `Session terminated.
<button class="btn-reconnect">Connect</button>`;
output.appendChild(discMsg);
discMsg.querySelector('.btn-reconnect').addEventListener('click', () => {
killed = false;
btnKill.style.background = '#ff5f56';
inputbar.style.display = 'flex';
discMsg.style.display = 'none';
input.disabled = false;
input.focus();
});
input.addEventListener('keydown', async e => {
if (e.key === 'ArrowUp') {
e.preventDefault();
if (histIdx < history.length - 1) histIdx++;
input.value = history[history.length - 1 - histIdx] ?? '';
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (histIdx > 0) histIdx--;
else {
histIdx = -1;
input.value = '';
return;
}
input.value = history[history.length - 1 - histIdx] ?? '';
return;
}
if (e.key !== 'Enter') return;
const cmd = input.value.trim();
input.value = '';
histIdx = -1;
if (!cmd) return;
history.push(cmd);
const entry = document.createElement('div');
entry.className = 'entry';
const promptLine = document.createElement('div');
promptLine.className = 'prompt-line';
promptLine.textContent = cmd;
entry.appendChild(promptLine);
if (cmd === 'clear' || cmd === 'cls') {
// clear all entries but keep welcome + discMsg
output.querySelectorAll('.entry').forEach(n => n.remove());
return;
}
const resultLine = document.createElement('div');
resultLine.className = 'result';
resultLine.textContent = '…';
entry.appendChild(resultLine);
output.insertBefore(entry, discMsg);
output.scrollTop = output.scrollHeight;
try {
const fd = new FormData();
fd.append('cmd', cmd);
const res = await fetch(window.location.pathname, {
method: 'POST',
body: fd
});
const text = await res.text();
resultLine.textContent = text || '';
if (!text.trim()) {
resultLine.textContent = '(no output)';
resultLine.classList.add('empty');
}
} catch (err) {
resultLine.textContent = 'Request failed: ' + err.message;
resultLine.style.color = 'var(--error)';
}
output.scrollTop = output.scrollHeight;
});
return {
id,
el,
output,
inputbar,
input,
discMsg,
kill() {
killed = true;
inputbar.style.display = 'none';
discMsg.style.display = 'block';
input.disabled = true;
output.scrollTop = output.scrollHeight;
},
focus() {
input.focus();
}
};
}
// tabs
function createTab(label) {
const id = ++tabCounter;
const pane = createPane();
// tab element
const tabEl = document.createElement('div');
tabEl.className = 'tab';
tabEl.dataset.tabId = id;
const labelEl = document.createElement('span');
labelEl.textContent = label || `bash ${id}`;
const closeEl = document.createElement('span');
closeEl.className = 'tab-close';
closeEl.textContent = '×';
closeEl.title = 'Close tab';
closeEl.addEventListener('click', e => {
e.stopPropagation();
closeTab(id);
});
tabEl.appendChild(labelEl);
tabEl.appendChild(closeEl);
tabEl.addEventListener('click', () => activateTab(id));
// insert before + button
tabbar.insertBefore(tabEl, btnNewTab);
const tab = {
id,
label: label || `bash ${id}`,
tabEl,
panes: [pane]
};
tabs.push(tab);
return tab;
}
function closeTab(id) {
if (tabs.length === 1) return; // keep at least one
const idx = tabs.findIndex(t => t.id === id);
if (idx === -1) return;
tabs[idx].tabEl.remove();
tabs.splice(idx, 1);
if (activeTabId === id) {
activateTab(tabs[Math.min(idx, tabs.length - 1)].id);
}
updateNewTabBtn();
}
function activateTab(id) {
activeTabId = id;
tabs.forEach(t => t.tabEl.classList.toggle('active', t.id === id));
renderSplitContainer();
}
function updateNewTabBtn() {
const full = tabs.length >= MAX_TABS;
btnNewTab.classList.toggle('disabled', full);
btnNewTab.title = full ? `Max ${MAX_TABS} tabs` : 'New tab';
}
// split terminal
function getActiveTab() {
return tabs.find(t => t.id === activeTabId);
}
function renderSplitContainer() {
splitContainer.innerHTML = '';
const tab = getActiveTab();
if (!tab) return;
tab.panes.forEach((pane, i) => {
if (i > 0) {
const resizer = document.createElement('div');
resizer.className = 'resizer';
initResizer(resizer, tab, i);
splitContainer.appendChild(resizer);
}
splitContainer.appendChild(pane.el);
});
updateSplitBtn();
// focus last pane
tab.panes[tab.panes.length - 1].focus();
}
function updateSplitBtn() {
const tab = getActiveTab();
const full = tab && tab.panes.length >= 3;
btnSplit.classList.toggle('disabled', full);
btnSplit.title = full ? 'Max 3 splits' : 'Split pane';
}
function addSplit() {
const tab = getActiveTab();
if (!tab || tab.panes.length >= 3) return;
const pane = createPane();
tab.panes.push(pane);
renderSplitContainer();
}
// resizer functions
function initResizer(resizer, tab, paneIdx) {
let startX, startWidths;
resizer.addEventListener('mousedown', e => {
e.preventDefault();
resizer.classList.add('dragging');
startX = e.clientX;
startWidths = tab.panes.map(p => p.el.getBoundingClientRect().width);
const onMove = e => {
const dx = e.clientX - startX;
const total = startWidths.reduce((a, b) => a + b, 0);
const leftW = Math.max(80, startWidths[paneIdx - 1] + dx);
const rightW = Math.max(80, startWidths[paneIdx] - dx);
const leftPct = (leftW / total * 100).toFixed(2);
const rightPct = (rightW / total * 100).toFixed(2);
tab.panes[paneIdx - 1].el.style.flex = `0 0 ${leftPct}%`;
tab.panes[paneIdx].el.style.flex = `0 0 ${rightPct}%`;
};
const onUp = () => {
resizer.classList.remove('dragging');
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
});
}
// kill the active pain
btnKill.addEventListener('click', () => {
const tab = getActiveTab();
if (!tab) return;
// kill the focused/last pane
tab.panes[tab.panes.length - 1].kill();
btnKill.style.background = '#555';
});
// spawn a new tab
btnNewTab.addEventListener('click', () => {
if (tabs.length >= MAX_TABS) return;
const tab = createTab();
activateTab(tab.id);
updateNewTabBtn();
});
// Split btn
btnSplit.addEventListener('click', () => addSplit());
// popout btn
const btnPopout = document.getElementById('btn-popout');
const isPopout = new URLSearchParams(window.location.search).has('popout');
if (isPopout) {
document.body.classList.add('popout');
document.title = 'terminal — ' + hostname;
btnPopout.style.display = 'none';
}
let popoutWin = null;
btnPopout.addEventListener('click', () => {
if (popoutWin && !popoutWin.closed) {
popoutWin.focus();
return;
}
const w = 800,
h = 520;
const left = Math.round(screen.width / 2 - w / 2);
const top = Math.round(screen.height / 2 - h / 2);
popoutWin = window.open(window.location.pathname + '?popout', `terminal_popout_${Date.now()}`, `width=${w},height=${h},left=${left},top=${top},resizable=yes,scrollbars=no,toolbar=no,menubar=no,location=no,status=no`);
if (!popoutWin) {
alert('Pop-up blocked! Allow pop-ups for this site.');
return;
}
btnPopout.textContent = '⤢ focus';
btnPopout.title = 'Bring popout to front';
const checkClosed = setInterval(() => {
if (popoutWin.closed) {
clearInterval(checkClosed);
btnPopout.textContent = '⤢ popout';
btnPopout.title = 'Pop out tab';
popoutWin = null;
}
}, 500);
});
// Init
const first = createTab('bash 1');
activateTab(first.id);
updateNewTabBtn();
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment