830cb5d679
Stack ttyd + tmux + Caddy, reproduction d'une installation en production. Inclut le frontend personnalise et les correctifs deja valides (FitAddon, refit apres changement de police, double collage Ctrl+V, OSC 52, window.open). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
325 lines
16 KiB
HTML
325 lines
16 KiB
HTML
<style>
|
||
#ttyd-bar{position:fixed;top:0;left:0;right:0;height:30px;background:#111;display:flex;align-items:center;padding:0 6px;gap:4px;z-index:8000;border-bottom:1px solid #2a2a2a;}
|
||
#ttyd-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;white-space:nowrap;}
|
||
#ttyd-bar button:hover{background:#333;color:#fff;}
|
||
#ttyd-bar select{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 4px;font:11px monospace;cursor:pointer;}
|
||
#ttyd-conn{width:8px;height:8px;border-radius:50%;background:#555;margin:0 4px;flex-shrink:0;}
|
||
#ttyd-conn.ok{background:#4caf50;}
|
||
#ttyd-conn.err{background:#f44336;animation:blink 1s infinite;}
|
||
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
||
#ttyd-spacer{flex:1;}
|
||
#ttyd-search-bar{position:fixed;top:30px;right:0;background:#1a1a1a;border:1px solid #333;border-top:none;border-right:none;border-radius:0 0 0 6px;display:flex;align-items:center;gap:4px;padding:4px 8px;z-index:8001;display:none;}
|
||
#ttyd-search-bar input{background:#111;color:#ddd;border:1px solid #333;border-radius:3px;padding:2px 6px;font:12px monospace;width:200px;outline:none;}
|
||
#ttyd-search-bar span{color:#666;font:11px monospace;min-width:60px;}
|
||
#ttyd-search-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;}
|
||
#terminal-container{position:absolute !important;top:30px;left:0;right:0;bottom:0;height:auto !important;padding-top:0 !important;}
|
||
#ttyd-ctx{position:fixed;background:#1e1e1e;border:1px solid #444;border-radius:4px;padding:4px 0;z-index:9999;min-width:150px;box-shadow:0 4px 12px rgba(0,0,0,.5);display:none;}
|
||
#ttyd-ctx div{padding:5px 14px;cursor:pointer;color:#ccc;font:12px monospace;}
|
||
#ttyd-ctx div:hover{background:#2d2d2d;color:#fff;}
|
||
#ttyd-ctx hr{border:none;border-top:1px solid #333;margin:3px 0;}
|
||
#ttyd-toast{position:fixed;bottom:8px;right:8px;background:rgba(20,20,20,.93);color:#7fff7f;font:12px monospace;padding:4px 10px;border-radius:4px;z-index:9999;pointer-events:none;opacity:0;transition:opacity .2s;}
|
||
#ttyd-toast.show{opacity:1;}
|
||
</style>
|
||
<div id="ttyd-bar">
|
||
<div id="ttyd-conn" title="Connexion WebSocket"></div>
|
||
<button onclick="ttydFontSize(-1)" title="Police -">A−</button>
|
||
<span id="ttyd-fsize" style="color:#555;font:11px monospace;min-width:28px;text-align:center;">13</span>
|
||
<button onclick="ttydFontSize(+1)" title="Police +">A+</button>
|
||
<button onclick="ttydClear()" title="Vider">✕ Clear</button>
|
||
<button onclick="ttydSearch()" title="Rechercher (Ctrl+F)">⌕ Rech.</button>
|
||
<select id="ttyd-theme-sel" onchange="ttydSetTheme(this.value)" title="Thème"></select>
|
||
<button onclick="ttydSplit('h')" title="Split vertical (côte à côte)">⬜ ⬜</button>
|
||
<button onclick="ttydSplit('v')" title="Split horizontal (haut/bas)">⬜<br style='line-height:3px'>⬜</button>
|
||
<button onclick="ttydPaneClose()" title="Fermer ce panneau">✕ Pan.</button>
|
||
<button onclick="ttydPaneNav('Left')" title="Panneau gauche">◀</button>
|
||
<button onclick="ttydPaneNav('Right')" title="Panneau droite">▶</button>
|
||
<button onclick="ttydPaneNav('Up')" title="Panneau haut">▲</button>
|
||
<button onclick="ttydPaneNav('Down')" title="Panneau bas">▼</button>
|
||
<div id="ttyd-spacer"></div>
|
||
<span style="color:#333;font:10px monospace;">Ctrl+C=copier/inter. Ctrl+V=coller clic droit=menu</span>
|
||
</div>
|
||
<div id="ttyd-search-bar">
|
||
<input id="ttyd-sq" placeholder="Rechercher…" oninput="ttydDoSearch()" onkeydown="ttydSearchKey(event)">
|
||
<span id="ttyd-sr">—</span>
|
||
<button onclick="ttydSearchNav(-1)">▲</button>
|
||
<button onclick="ttydSearchNav(+1)">▼</button>
|
||
<button onclick="ttydSearchClose()">✕</button>
|
||
</div>
|
||
<div id="ttyd-ctx"></div>
|
||
<div id="ttyd-toast"></div>
|
||
<script>
|
||
(function(){
|
||
var _fontSize = parseInt(localStorage.getItem('ttyd-font') || '13');
|
||
var _themes = {"Defaut": {"background": "#2b2b2b", "foreground": "#d2d2d2", "cursor": "#adadad", "black": "#000000", "red": "#d81e00", "green": "#5ea702", "yellow": "#cfae00", "blue": "#427ab3", "magenta": "#89658e", "cyan": "#00a7aa", "white": "#dbded8", "brightBlack": "#686a66", "brightRed": "#f54235", "brightGreen": "#99e343", "brightYellow": "#fdeb61", "brightBlue": "#84b0d8", "brightMagenta": "#bc94b7", "brightCyan": "#37e6e8", "brightWhite": "#f1f1f0"}, "Dracula": {"background": "#282a36", "foreground": "#f8f8f2", "cursor": "#f8f8f2", "black": "#21222c", "red": "#ff5555", "green": "#50fa7b", "yellow": "#f1fa8c", "blue": "#bd93f9", "magenta": "#ff79c6", "cyan": "#8be9fd", "white": "#f8f8f2", "brightBlack": "#6272a4", "brightRed": "#ff6e6e", "brightGreen": "#69ff94", "brightYellow": "#ffffa5", "brightBlue": "#d6acff", "brightMagenta": "#ff92df", "brightCyan": "#a4ffff", "brightWhite": "#ffffff"}, "One Dark": {"background": "#282c34", "foreground": "#abb2bf", "cursor": "#528bff", "black": "#3f4451", "red": "#e06c75", "green": "#98c379", "yellow": "#e5c07b", "blue": "#61afef", "magenta": "#c678dd", "cyan": "#56b6c2", "white": "#abb2bf", "brightBlack": "#4f5666", "brightRed": "#ff616e", "brightGreen": "#a5e075", "brightYellow": "#f0a45d", "brightBlue": "#4dc4ff", "brightMagenta": "#de73ff", "brightCyan": "#4cd1e0", "brightWhite": "#ffffff"}, "Solarized": {"background": "#002b36", "foreground": "#839496", "cursor": "#839496", "black": "#073642", "red": "#dc322f", "green": "#859900", "yellow": "#b58900", "blue": "#268bd2", "magenta": "#d33682", "cyan": "#2aa198", "white": "#eee8d5", "brightBlack": "#002b36", "brightRed": "#cb4b16", "brightGreen": "#586e75", "brightYellow": "#657b83", "brightBlue": "#839496", "brightMagenta": "#6c71c4", "brightCyan": "#93a1a1", "brightWhite": "#fdf6e3"}, "Clair": {"background": "#ffffff", "foreground": "#383a42", "cursor": "#526fff", "black": "#383a42", "red": "#e45649", "green": "#50a14f", "yellow": "#c18401", "blue": "#4078f2", "magenta": "#a626a4", "cyan": "#0184bc", "white": "#fafafa", "brightBlack": "#4f525e", "brightRed": "#e06c75", "brightGreen": "#98c379", "brightYellow": "#e5c07b", "brightBlue": "#61afef", "brightMagenta": "#c678dd", "brightCyan": "#56b6c2", "brightWhite": "#ffffff"}};
|
||
var _themeName = localStorage.getItem('ttyd-theme') || 'Defaut';
|
||
var _searchMatches = [], _searchIdx = 0;
|
||
var _notifPerm = false;
|
||
var _lastCmdStart = 0;
|
||
|
||
/* ---- Toast ---- */
|
||
function toast(msg, dur) {
|
||
var t = document.getElementById('ttyd-toast');
|
||
t.textContent = msg; t.classList.add('show');
|
||
clearTimeout(t._tid);
|
||
t._tid = setTimeout(function(){ t.classList.remove('show'); }, dur||1800);
|
||
}
|
||
|
||
/* ---- Clipboard ---- */
|
||
function writeClip(text) {
|
||
if (!text) return;
|
||
var ok = function(){ toast('Copié : ' + text.slice(0,40).replace(/\n/g,' ') + (text.length>40?'..':'')); };
|
||
if (navigator.clipboard && navigator.clipboard.writeText)
|
||
navigator.clipboard.writeText(text).then(ok).catch(function(){ legacyCopy(text); });
|
||
else legacyCopy(text);
|
||
}
|
||
function legacyCopy(text) {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text; ta.style.cssText='position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;';
|
||
document.body.appendChild(ta); ta.focus(); ta.select();
|
||
var ok=false; try{ ok=document.execCommand('copy'); }catch(e){}
|
||
document.body.removeChild(ta);
|
||
toast(ok ? 'Copié (legacy)' : 'Erreur copie');
|
||
}
|
||
function readClip(cb) {
|
||
if (navigator.clipboard && navigator.clipboard.readText)
|
||
navigator.clipboard.readText().then(cb).catch(function(){ toast('Accès presse-papier refusé'); });
|
||
else toast('API clipboard non disponible');
|
||
}
|
||
|
||
/* ---- Police ---- */
|
||
window.ttydFontSize = function(d) {
|
||
_fontSize = Math.max(8, Math.min(28, _fontSize + d));
|
||
localStorage.setItem('ttyd-font', _fontSize);
|
||
if (window.term) { window.term.options.fontSize = _fontSize; window.dispatchEvent(new Event('resize')); }
|
||
document.getElementById('ttyd-fsize').textContent = _fontSize;
|
||
};
|
||
|
||
/* ---- Clear ---- */
|
||
window.ttydClear = function() { if (window.term) window.term.clear(); };
|
||
|
||
/* ---- Tmux split & navigation ---- */
|
||
function sendRaw(data) {
|
||
// Envoie données brutes au PTY sans bracketed paste
|
||
var term = window.term;
|
||
if (!term) return;
|
||
if (term._core && term._core.coreService)
|
||
term._core.coreService.triggerDataEvent(data, true);
|
||
else
|
||
term.paste(data);
|
||
}
|
||
window.ttydSplit = function(dir) {
|
||
// Ctrl+B (\x02) + % pour vertical, + " pour horizontal
|
||
sendRaw('\x02' + (dir === 'h' ? '%' : '"'));
|
||
};
|
||
window.ttydPaneClose = function() {
|
||
// Ctrl+B + x, puis y pour confirmer
|
||
sendRaw('\x02x');
|
||
setTimeout(function(){ sendRaw('y'); }, 200);
|
||
};
|
||
window.ttydPaneNav = function(dir) {
|
||
var arrows = {Left:'\x1b[D', Right:'\x1b[C', Up:'\x1b[A', Down:'\x1b[B'};
|
||
if (arrows[dir]) sendRaw('\x02' + arrows[dir]);
|
||
};
|
||
|
||
/* ---- Thèmes ---- */
|
||
var sel = document.getElementById('ttyd-theme-sel');
|
||
Object.keys(_themes).forEach(function(n){
|
||
var o = document.createElement('option'); o.value=n; o.textContent=n;
|
||
if(n===_themeName) o.selected=true;
|
||
sel.appendChild(o);
|
||
});
|
||
window.ttydSetTheme = function(name) {
|
||
if (!_themes[name] || !window.term) return;
|
||
window.term.options.theme = _themes[name];
|
||
_themeName = name;
|
||
localStorage.setItem('ttyd-theme', name);
|
||
toast('Thème : ' + name);
|
||
};
|
||
|
||
/* ---- Recherche ---- */
|
||
var _searchOpen = false;
|
||
window.ttydSearch = function() {
|
||
var bar = document.getElementById('ttyd-search-bar');
|
||
_searchOpen = !_searchOpen;
|
||
bar.style.display = _searchOpen ? 'flex' : 'none';
|
||
if (_searchOpen) setTimeout(function(){ document.getElementById('ttyd-sq').focus(); }, 50);
|
||
};
|
||
window.ttydSearchClose = function() {
|
||
_searchOpen = false;
|
||
document.getElementById('ttyd-search-bar').style.display = 'none';
|
||
_searchMatches = []; document.getElementById('ttyd-sr').textContent = '—';
|
||
};
|
||
window.ttydDoSearch = function() {
|
||
var q = document.getElementById('ttyd-sq').value;
|
||
var sr = document.getElementById('ttyd-sr');
|
||
if (!q || !window.term) { _searchMatches=[]; sr.textContent='—'; return; }
|
||
var buf = window.term.buffer.active;
|
||
var matches = [];
|
||
var total = buf.length;
|
||
for (var i = 0; i < total; i++) {
|
||
var line = buf.getLine(i);
|
||
if (line && line.translateToString().indexOf(q) >= 0) matches.push(i);
|
||
}
|
||
_searchMatches = matches; _searchIdx = matches.length - 1;
|
||
sr.textContent = matches.length ? ((_searchIdx+1)+'/'+matches.length) : '0 résultat';
|
||
if (matches.length) window.term.scrollToLine(matches[_searchIdx]);
|
||
};
|
||
window.ttydSearchNav = function(dir) {
|
||
if (!_searchMatches.length) return;
|
||
_searchIdx = (_searchIdx + dir + _searchMatches.length) % _searchMatches.length;
|
||
document.getElementById('ttyd-sr').textContent = (_searchIdx+1)+'/'+_searchMatches.length;
|
||
window.term.scrollToLine(_searchMatches[_searchIdx]);
|
||
};
|
||
window.ttydSearchKey = function(e) {
|
||
if (e.key === 'Enter') ttydSearchNav(e.shiftKey ? -1 : 1);
|
||
if (e.key === 'Escape') ttydSearchClose();
|
||
};
|
||
|
||
/* ---- Connexion ---- */
|
||
var _connEl = document.getElementById('ttyd-conn');
|
||
function checkConn() {
|
||
fetch('/token', {cache:'no-store'})
|
||
.then(function(r){ _connEl.className = r.ok ? 'ok' : 'err'; })
|
||
.catch(function(){ _connEl.className = 'err'; });
|
||
}
|
||
checkConn();
|
||
setInterval(checkConn, 5000);
|
||
|
||
/* ---- Patch window.open (liens WebLinksAddon) ---- */
|
||
var _open = window.open;
|
||
window.open = function(url, target, features) {
|
||
if (url === undefined || url === null || url === '') {
|
||
var loc = {};
|
||
Object.defineProperty(loc,'href',{set:function(u){
|
||
var a=document.createElement('a'); a.href=u; a.target='_blank'; a.rel='noopener noreferrer';
|
||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||
}});
|
||
return {opener:null, location:loc};
|
||
}
|
||
return _open.apply(this, arguments);
|
||
};
|
||
|
||
/* ---- Menu contextuel ---- */
|
||
var _ctx = document.getElementById('ttyd-ctx');
|
||
function hideCtx(){ _ctx.style.display='none'; }
|
||
document.addEventListener('click', hideCtx);
|
||
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ hideCtx(); ttydSearchClose(); }});
|
||
function showCtx(x,y) {
|
||
hideCtx();
|
||
_ctx.innerHTML = '';
|
||
var items = [
|
||
['Copier', function(){ if(window.term){var s=window.term.getSelection();if(s)writeClip(s);} }],
|
||
['Coller', function(){ if(window.term) readClip(function(t){window.term.paste(t);}); }],
|
||
null,
|
||
['Rechercher…', ttydSearch],
|
||
null,
|
||
['Effacer', ttydClear],
|
||
['Police +', function(){ttydFontSize(+1);}],
|
||
['Police −', function(){ttydFontSize(-1);}],
|
||
];
|
||
items.forEach(function(item){
|
||
if(!item){var hr=document.createElement('hr');_ctx.appendChild(hr);return;}
|
||
var d=document.createElement('div'); d.textContent=item[0];
|
||
d.addEventListener('click',function(e){e.stopPropagation();hideCtx();item[1]();});
|
||
_ctx.appendChild(d);
|
||
});
|
||
_ctx.style.cssText='display:block;left:'+x+'px;top:'+y+'px;';
|
||
var r=_ctx.getBoundingClientRect();
|
||
if(r.right>window.innerWidth) _ctx.style.left=(x-r.width)+'px';
|
||
if(r.bottom>window.innerHeight) _ctx.style.top=(y-r.height)+'px';
|
||
}
|
||
|
||
/* ---- Notifications ---- */
|
||
function requestNotifPerm() {
|
||
if ('Notification' in window && Notification.permission === 'default')
|
||
Notification.requestPermission().then(function(p){ _notifPerm = p==='granted'; });
|
||
else _notifPerm = ('Notification' in window && Notification.permission === 'granted');
|
||
}
|
||
function desktopNotif(title, body) {
|
||
if (_notifPerm && 'Notification' in window)
|
||
new Notification(title, {body:body, icon:''});
|
||
}
|
||
requestNotifPerm();
|
||
|
||
/* ---- Initialisation terminal ---- */
|
||
function waitForTerm() {
|
||
var term = window.term;
|
||
if (!term || !term.parser) { setTimeout(waitForTerm, 100); return; }
|
||
|
||
/* Police initiale */
|
||
term.options.fontSize = _fontSize;
|
||
document.getElementById('ttyd-fsize').textContent = _fontSize;
|
||
|
||
/* Thème initial */
|
||
if (_themes[_themeName]) term.options.theme = _themes[_themeName];
|
||
|
||
/* Refit : le fit initial a ete calcule avec l'ancienne taille de cellule */
|
||
setTimeout(function(){ window.dispatchEvent(new Event('resize')); }, 50);
|
||
|
||
/* OSC 52 : tmux set-clipboard -> presse-papier */
|
||
try {
|
||
term.parser.registerOscHandler(52, function(data) {
|
||
var parts = data.split(';');
|
||
if (parts.length>=2 && parts[1] && parts[1]!=='?') {
|
||
try { writeClip(atob(parts[1])); } catch(e){}
|
||
}
|
||
return true;
|
||
});
|
||
} catch(e){}
|
||
|
||
/* OSC 777 : PROMPT_COMMAND -> notification fin de commande */
|
||
try {
|
||
term.parser.registerOscHandler(777, function(data) {
|
||
var code = parseInt(data) || 0;
|
||
var elapsed = Date.now() - _lastCmdStart;
|
||
/* Notifier si onglet caché ET commande > 5s */
|
||
if (document.hidden && elapsed > 5000) {
|
||
var msg = code === 0 ? 'Commande terminée (succès)' : 'Commande terminée (code '+code+')';
|
||
desktopNotif('Terminal', msg);
|
||
}
|
||
_lastCmdStart = Date.now();
|
||
return true;
|
||
});
|
||
} catch(e){}
|
||
|
||
/* Ctrl+C = copie si sélection, sinon SIGINT */
|
||
/* Ctrl+V = colle */
|
||
/* Ctrl+F = recherche */
|
||
term.attachCustomKeyEventHandler(function(e) {
|
||
if (e.type !== 'keydown') return true;
|
||
if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||
if (e.code==='KeyC') {
|
||
var s = term.getSelection ? term.getSelection() : '';
|
||
if (s) { writeClip(s); return false; }
|
||
return true;
|
||
}
|
||
if (e.code==='KeyV') {
|
||
e.preventDefault();
|
||
readClip(function(t){ term.paste(t); }); return false;
|
||
}
|
||
if (e.code==='KeyF') { ttydSearch(); return false; }
|
||
}
|
||
if (e.ctrlKey && e.shiftKey) {
|
||
if (e.code==='KeyC') { var s=term.getSelection?term.getSelection():''; if(s) writeClip(s); return false; }
|
||
if (e.code==='KeyV') { e.preventDefault(); readClip(function(t){ term.paste(t); }); return false; }
|
||
}
|
||
return true;
|
||
});
|
||
|
||
/* Clic droit -> menu contextuel */
|
||
term.element.addEventListener('contextmenu', function(e){
|
||
e.preventDefault(); showCtx(e.clientX, e.clientY);
|
||
});
|
||
|
||
/* Zoom souris */
|
||
term.element.addEventListener('wheel', function(e){
|
||
if (!e.ctrlKey) return;
|
||
e.preventDefault();
|
||
ttydFontSize(e.deltaY < 0 ? 1 : -1);
|
||
}, {passive:false});
|
||
}
|
||
waitForTerm();
|
||
|
||
|
||
})();
|
||
</script> |